blob: ac7494c4759ef919a95f583eb5c0a66484d61a64 [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
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02008import collections
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00009import contextlib
hinoka@google.com267f33e2014-02-28 22:02:32 +000010import cStringIO
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000011import datetime
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000012import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020013import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000015import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000016import platform
maruel@chromium.org3742c842010-09-09 19:27:14 +000017import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000018import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000019import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000020import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000021import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000022import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000023import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000024import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000025import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000027import subprocess2
28
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000029
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000030RETRY_MAX = 3
31RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000032START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000033
34
borenet@google.com6a9b1682014-03-24 18:35:23 +000035_WARNINGS = []
36
37
szager@chromium.orgff113292014-03-25 06:02:08 +000038# These repos are known to cause OOM errors on 32-bit platforms, due the the
39# very large objects they contain. It is not safe to use threaded index-pack
40# when cloning/fetching them.
41THREADED_INDEX_PACK_BLACKLIST = [
42 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
43]
44
45
maruel@chromium.org66c83e62010-09-07 14:18:45 +000046class Error(Exception):
47 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000048 def __init__(self, msg, *args, **kwargs):
49 index = getattr(threading.currentThread(), 'index', 0)
50 if index:
51 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
52 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000053
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000054
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000055def Elapsed(until=None):
56 if until is None:
57 until = datetime.datetime.now()
58 return str(until - START).partition('.')[0]
59
60
borenet@google.com6a9b1682014-03-24 18:35:23 +000061def PrintWarnings():
62 """Prints any accumulated warnings."""
63 if _WARNINGS:
64 print >> sys.stderr, '\n\nWarnings:'
65 for warning in _WARNINGS:
66 print >> sys.stderr, warning
67
68
69def AddWarning(msg):
70 """Adds the given warning message to the list of accumulated warnings."""
71 _WARNINGS.append(msg)
72
73
msb@chromium.orgac915bb2009-11-13 17:03:01 +000074def SplitUrlRevision(url):
75 """Splits url and returns a two-tuple: url, rev"""
76 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000077 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000078 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000079 components = re.search(regex, url).groups()
80 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +000081 components = url.rsplit('@', 1)
82 if re.match(r'^\w+\@', url) and '@' not in components[0]:
83 components = [url]
84
msb@chromium.orgac915bb2009-11-13 17:03:01 +000085 if len(components) == 1:
86 components += [None]
87 return tuple(components)
88
89
primiano@chromium.org5439ea52014-08-06 17:18:18 +000090def IsGitSha(revision):
91 """Returns true if the given string is a valid hex-encoded sha"""
92 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
93
94
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +020095def IsFullGitSha(revision):
96 """Returns true if the given string is a valid hex-encoded full sha"""
97 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
98
99
floitsch@google.comeaab7842011-04-28 09:07:58 +0000100def IsDateRevision(revision):
101 """Returns true if the given revision is of the form "{ ... }"."""
102 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
103
104
105def MakeDateRevision(date):
106 """Returns a revision representing the latest revision before the given
107 date."""
108 return "{" + date + "}"
109
110
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000111def SyntaxErrorToError(filename, e):
112 """Raises a gclient_utils.Error exception with the human readable message"""
113 try:
114 # Try to construct a human readable error message
115 if filename:
116 error_message = 'There is a syntax error in %s\n' % filename
117 else:
118 error_message = 'There is a syntax error\n'
119 error_message += 'Line #%s, character %s: "%s"' % (
120 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
121 except:
122 # Something went wrong, re-raise the original exception
123 raise e
124 else:
125 raise Error(error_message)
126
127
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000128class PrintableObject(object):
129 def __str__(self):
130 output = ''
131 for i in dir(self):
132 if i.startswith('__'):
133 continue
134 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
135 return output
136
137
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000138def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000139 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000140 # codecs.open() has different behavior than open() on python 2.6 so use
141 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000142 s = f.read()
143 try:
144 return s.decode('utf-8')
145 except UnicodeDecodeError:
146 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000147
148
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000149def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000150 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000151 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000152
153
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000154@contextlib.contextmanager
155def temporary_directory(**kwargs):
156 tdir = tempfile.mkdtemp(**kwargs)
157 try:
158 yield tdir
159 finally:
160 if tdir:
161 rmtree(tdir)
162
163
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000164def safe_rename(old, new):
165 """Renames a file reliably.
166
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000167 Sometimes os.rename does not work because a dying git process keeps a handle
168 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000169 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000170 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000171 """
172 # roughly 10s
173 retries = 100
174 for i in range(retries):
175 try:
176 os.rename(old, new)
177 break
178 except OSError:
179 if i == (retries - 1):
180 # Give up.
181 raise
182 # retry
183 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
184 time.sleep(0.1)
185
186
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000187def rm_file_or_tree(path):
188 if os.path.isfile(path):
189 os.remove(path)
190 else:
191 rmtree(path)
192
193
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000194def rmtree(path):
195 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000196
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000197 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000198
199 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700200 are read-only. We need to be able to force the files to be writable (i.e.,
201 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000202
203 Even with all this, Windows still sometimes fails to delete a file, citing
204 a permission error (maybe something to do with antivirus scans or disk
205 indexing). The best suggestion any of the user forums had was to wait a
206 bit and try again, so we do that too. It's hand-waving, but sometimes it
207 works. :/
208
209 On POSIX systems, things are a little bit simpler. The modes of the files
210 to be deleted doesn't matter, only the modes of the directories containing
211 them are significant. As the directory tree is traversed, each directory
212 has its mode set appropriately before descending into it. This should
213 result in the entire tree being removed, with the possible exception of
214 *path itself, because nothing attempts to change the mode of its parent.
215 Doing so would be hazardous, as it's not a directory slated for removal.
216 In the ordinary case, this is not a problem: for our purposes, the user
217 will never lack write permission on *path's parent.
218 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000219 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220 return
221
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000222 if os.path.islink(path) or not os.path.isdir(path):
223 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000225 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000226 # Give up and use cmd.exe's rd command.
227 path = os.path.normcase(path)
228 for _ in xrange(3):
229 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
230 if exitcode == 0:
231 return
232 else:
233 print >> sys.stderr, 'rd exited with code %d' % exitcode
234 time.sleep(3)
235 raise Exception('Failed to remove path %s' % path)
236
237 # On POSIX systems, we need the x-bit set on the directory to access it,
238 # the r-bit to see its contents, and the w-bit to remove files from it.
239 # The actual modes of the files within the directory is irrelevant.
240 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000241
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000242 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000243 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000244
245 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000246 # If fullpath is a symbolic link that points to a directory, isdir will
247 # be True, but we don't want to descend into that as a directory, we just
248 # want to remove the link. Check islink and treat links as ordinary files
249 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000250 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000251 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000252 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000253 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000254 # Recurse.
255 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000256
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000257 remove(os.rmdir, path)
258
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000259
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000260def safe_makedirs(tree):
261 """Creates the directory in a safe manner.
262
263 Because multiple threads can create these directories concurently, trap the
264 exception and pass on.
265 """
266 count = 0
267 while not os.path.exists(tree):
268 count += 1
269 try:
270 os.makedirs(tree)
271 except OSError, e:
272 # 17 POSIX, 183 Windows
273 if e.errno not in (17, 183):
274 raise
275 if count > 40:
276 # Give up.
277 raise
278
279
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000280def CommandToStr(args):
281 """Converts an arg list into a shell escaped string."""
282 return ' '.join(pipes.quote(arg) for arg in args)
283
284
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000285def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000286 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000287
maruel@chromium.org17d01792010-09-01 18:07:10 +0000288 If |always| is True, a message indicating what is being done
289 is printed to stdout all the time even if not output is generated. Otherwise
290 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000291 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000292 stdout = kwargs.setdefault('stdout', sys.stdout)
293 if header is None:
294 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000295 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000296
maruel@chromium.org17d01792010-09-01 18:07:10 +0000297 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000298 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000299 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000300 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000301 def filter_msg(line):
302 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000303 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000304 elif filter_fn:
305 filter_fn(line)
306 kwargs['filter_fn'] = filter_msg
307 kwargs['call_filter_on_first_line'] = True
308 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000309 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000310 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000311
maruel@chromium.org17d01792010-09-01 18:07:10 +0000312
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000313class Wrapper(object):
314 """Wraps an object, acting as a transparent proxy for all properties by
315 default.
316 """
317 def __init__(self, wrapped):
318 self._wrapped = wrapped
319
320 def __getattr__(self, name):
321 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000322
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000323
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000324class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000325 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000326 def __init__(self, wrapped, delay):
327 super(AutoFlush, self).__init__(wrapped)
328 if not hasattr(self, 'lock'):
329 self.lock = threading.Lock()
330 self.__last_flushed_at = time.time()
331 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000332
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000333 @property
334 def autoflush(self):
335 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000336
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000337 def write(self, out, *args, **kwargs):
338 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000339 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000340 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000341 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000342 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000343 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000344 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000345 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000346 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000347 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000348 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000349
350
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000351class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000352 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000353 threads with a NN> prefix.
354 """
355 def __init__(self, wrapped, include_zero=False):
356 super(Annotated, self).__init__(wrapped)
357 if not hasattr(self, 'lock'):
358 self.lock = threading.Lock()
359 self.__output_buffers = {}
360 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000361
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000362 @property
363 def annotated(self):
364 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000365
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000366 def write(self, out):
367 index = getattr(threading.currentThread(), 'index', 0)
368 if not index and not self.__include_zero:
369 # Unindexed threads aren't buffered.
370 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000371
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000372 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000373 try:
374 # Use a dummy array to hold the string so the code can be lockless.
375 # Strings are immutable, requiring to keep a lock for the whole dictionary
376 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000377 if not index in self.__output_buffers:
378 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000379 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000380 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000381 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000382 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000383
384 # Continue lockless.
385 obj[0] += out
386 while '\n' in obj[0]:
387 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000388 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000389 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000390 obj[0] = remaining
391
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000392 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000393 """Flush buffered output."""
394 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000395 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000396 try:
397 # Detect threads no longer existing.
398 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000399 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000400 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000401 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000402 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000403 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000404 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000405 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000406 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000407
408 # Don't keep the lock while writting. Will append \n when it shouldn't.
409 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000410 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000411 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
412 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000413
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000414
415def MakeFileAutoFlush(fileobj, delay=10):
416 autoflush = getattr(fileobj, 'autoflush', None)
417 if autoflush:
418 autoflush.delay = delay
419 return fileobj
420 return AutoFlush(fileobj, delay)
421
422
423def MakeFileAnnotated(fileobj, include_zero=False):
424 if getattr(fileobj, 'annotated', None):
425 return fileobj
426 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000427
428
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000429GCLIENT_CHILDREN = []
430GCLIENT_CHILDREN_LOCK = threading.Lock()
431
432
433class GClientChildren(object):
434 @staticmethod
435 def add(popen_obj):
436 with GCLIENT_CHILDREN_LOCK:
437 GCLIENT_CHILDREN.append(popen_obj)
438
439 @staticmethod
440 def remove(popen_obj):
441 with GCLIENT_CHILDREN_LOCK:
442 GCLIENT_CHILDREN.remove(popen_obj)
443
444 @staticmethod
445 def _attemptToKillChildren():
446 global GCLIENT_CHILDREN
447 with GCLIENT_CHILDREN_LOCK:
448 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
449
450 for zombie in zombies:
451 try:
452 zombie.kill()
453 except OSError:
454 pass
455
456 with GCLIENT_CHILDREN_LOCK:
457 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
458
459 @staticmethod
460 def _areZombies():
461 with GCLIENT_CHILDREN_LOCK:
462 return bool(GCLIENT_CHILDREN)
463
464 @staticmethod
465 def KillAllRemainingChildren():
466 GClientChildren._attemptToKillChildren()
467
468 if GClientChildren._areZombies():
469 time.sleep(0.5)
470 GClientChildren._attemptToKillChildren()
471
472 with GCLIENT_CHILDREN_LOCK:
473 if GCLIENT_CHILDREN:
474 print >> sys.stderr, 'Could not kill the following subprocesses:'
475 for zombie in GCLIENT_CHILDREN:
476 print >> sys.stderr, ' ', zombie.pid
477
478
maruel@chromium.org17d01792010-09-01 18:07:10 +0000479def CheckCallAndFilter(args, stdout=None, filter_fn=None,
480 print_stdout=None, call_filter_on_first_line=False,
tandrii64103db2016-10-11 05:30:05 -0700481 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000482 """Runs a command and calls back a filter function if needed.
483
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000484 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000485 print_stdout: If True, the command's stdout is forwarded to stdout.
486 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000487 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000488 character trimmed.
489 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000490 retry: If the process exits non-zero, sleep for a brief interval and try
491 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000492
493 stderr is always redirected to stdout.
494 """
495 assert print_stdout or filter_fn
496 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000497 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000498 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000499
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000500 sleep_interval = RETRY_INITIAL_SLEEP
501 run_cwd = kwargs.get('cwd', os.getcwd())
502 for _ in xrange(RETRY_MAX + 1):
503 kid = subprocess2.Popen(
504 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
505 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000506
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000507 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000508
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000509 # Do a flush of stdout before we begin reading from the subprocess2's stdout
510 stdout.flush()
511
512 # Also, we need to forward stdout to prevent weird re-ordering of output.
513 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700514 # normally buffering is done for each line, but if the process requests
515 # input, no end-of-line character is output after the prompt and it would
516 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000517 try:
518 in_byte = kid.stdout.read(1)
519 if in_byte:
520 if call_filter_on_first_line:
521 filter_fn(None)
522 in_line = ''
523 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000524 output.write(in_byte)
525 if print_stdout:
526 stdout.write(in_byte)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000527 if in_byte not in ['\r', '\n']:
528 in_line += in_byte
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000529 else:
530 filter_fn(in_line)
531 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000532 in_byte = kid.stdout.read(1)
533 # Flush the rest of buffered output. This is only an issue with
534 # stdout/stderr not ending with a \n.
535 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000536 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000537 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000538
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000539 # Don't put this in a 'finally,' since the child may still run if we get
540 # an exception.
541 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000542
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000543 except KeyboardInterrupt:
544 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
545 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000546
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000547 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000548 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000549 if not retry:
550 break
tandrii30d95622016-10-11 05:20:26 -0700551 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
552 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000553 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000554 sleep_interval *= 2
555 raise subprocess2.CalledProcessError(
556 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000557
558
agable@chromium.org5a306a22014-02-24 22:13:59 +0000559class GitFilter(object):
560 """A filter_fn implementation for quieting down git output messages.
561
562 Allows a custom function to skip certain lines (predicate), and will throttle
563 the output of percentage completed lines to only output every X seconds.
564 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000565 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000566
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000567 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000568 """
569 Args:
570 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
571 XX% complete messages) to only be printed at least |time_throttle|
572 seconds apart.
573 predicate (f(line)): An optional function which is invoked for every line.
574 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000575 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000576 """
577 self.last_time = 0
578 self.time_throttle = time_throttle
579 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000580 self.out_fh = out_fh or sys.stdout
581 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000582
583 def __call__(self, line):
584 # git uses an escape sequence to clear the line; elide it.
585 esc = line.find(unichr(033))
586 if esc > -1:
587 line = line[:esc]
588 if self.predicate and not self.predicate(line):
589 return
590 now = time.time()
591 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000592 if match:
593 if match.group(1) != self.progress_prefix:
594 self.progress_prefix = match.group(1)
595 elif now - self.last_time < self.time_throttle:
596 return
597 self.last_time = now
598 self.out_fh.write('[%s] ' % Elapsed())
599 print >> self.out_fh, line
agable@chromium.org5a306a22014-02-24 22:13:59 +0000600
601
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000602def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000603 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000604 real_from_dir = os.path.realpath(from_dir)
605 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000606 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000607 split_path = os.path.split(path)
608 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000609 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000610 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000611
612 # If we did not find the file in the current directory, make sure we are in a
613 # sub directory that is controlled by this configuration.
614 if path != real_from_dir:
615 entries_filename = os.path.join(path, filename + '_entries')
616 if not os.path.exists(entries_filename):
617 # If .gclient_entries does not exist, a previous call to gclient sync
618 # might have failed. In that case, we cannot verify that the .gclient
619 # is the one we want to use. In order to not to cause too much trouble,
620 # just issue a warning and return the path anyway.
Bruce Dawson1c5c1182017-06-13 10:34:06 -0700621 print >> sys.stderr, ("%s missing, %s file in parent directory %s might "
622 "not be the file you want to use." %
623 (entries_filename, filename, path))
jochen@chromium.org20760a52010-09-08 08:47:28 +0000624 return path
625 scope = {}
626 try:
627 exec(FileRead(entries_filename), scope)
628 except SyntaxError, e:
629 SyntaxErrorToError(filename, e)
630 all_directories = scope['entries'].keys()
631 path_to_check = real_from_dir[len(path)+1:]
632 while path_to_check:
633 if path_to_check in all_directories:
634 return path
635 path_to_check = os.path.dirname(path_to_check)
636 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000637
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000638 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000639 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000640
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000641
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000642def PathDifference(root, subpath):
643 """Returns the difference subpath minus root."""
644 root = os.path.realpath(root)
645 subpath = os.path.realpath(subpath)
646 if not subpath.startswith(root):
647 return None
648 # If the root does not have a trailing \ or /, we add it so the returned
649 # path starts immediately after the seperator regardless of whether it is
650 # provided.
651 root = os.path.join(root, '')
652 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000653
654
655def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000656 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000657
rcui@google.com13595ff2011-10-13 01:25:07 +0000658 Returns nearest upper-level directory with the passed in file.
659 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000660 if not path:
661 path = os.getcwd()
662 path = os.path.realpath(path)
663 while True:
664 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000665 if os.path.exists(file_path):
666 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000667 (new_path, _) = os.path.split(path)
668 if new_path == path:
669 return None
670 path = new_path
671
672
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000673def GetMacWinOrLinux():
674 """Returns 'mac', 'win', or 'linux', matching the current platform."""
675 if sys.platform.startswith(('cygwin', 'win')):
676 return 'win'
677 elif sys.platform.startswith('linux'):
678 return 'linux'
679 elif sys.platform == 'darwin':
680 return 'mac'
681 raise Error('Unknown platform: ' + sys.platform)
682
683
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000684def GetPrimarySolutionPath():
685 """Returns the full path to the primary solution. (gclient_root + src)"""
zturner@chromium.org0db9a142014-08-13 23:15:25 +0000686
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000687 gclient_root = FindGclientRoot(os.getcwd())
688 if not gclient_root:
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000689 # Some projects might not use .gclient. Try to see whether we're in a git
690 # checkout.
691 top_dir = [os.getcwd()]
692 def filter_fn(line):
hanpfei13f9c372016-08-08 22:05:56 -0700693 repo_root_path = os.path.normpath(line.rstrip('\n'))
694 if os.path.exists(repo_root_path):
695 top_dir[0] = repo_root_path
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000696 try:
697 CheckCallAndFilter(["git", "rev-parse", "--show-toplevel"],
698 print_stdout=False, filter_fn=filter_fn)
699 except Exception:
700 pass
701 top_dir = top_dir[0]
702 if os.path.exists(os.path.join(top_dir, 'buildtools')):
jiangj@opera.comd6d15b82015-04-20 06:43:48 +0000703 return top_dir
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000704 return None
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000705
706 # Some projects' top directory is not named 'src'.
707 source_dir_name = GetGClientPrimarySolutionName(gclient_root) or 'src'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000708 return os.path.join(gclient_root, source_dir_name)
709
710
711def GetBuildtoolsPath():
712 """Returns the full path to the buildtools directory.
713 This is based on the root of the checkout containing the current directory."""
714
715 # Overriding the build tools path by environment is highly unsupported and may
716 # break without warning. Do not rely on this for anything important.
717 override = os.environ.get('CHROMIUM_BUILDTOOLS_PATH')
718 if override is not None:
719 return override
720
721 primary_solution = GetPrimarySolutionPath()
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000722 if not primary_solution:
723 return None
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000724 buildtools_path = os.path.join(primary_solution, 'buildtools')
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000725 if not os.path.exists(buildtools_path):
726 # Buildtools may be in the gclient root.
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000727 gclient_root = FindGclientRoot(os.getcwd())
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000728 buildtools_path = os.path.join(gclient_root, 'buildtools')
729 return buildtools_path
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000730
731
732def GetBuildtoolsPlatformBinaryPath():
733 """Returns the full path to the binary directory for the current platform."""
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000734 buildtools_path = GetBuildtoolsPath()
735 if not buildtools_path:
736 return None
737
738 if sys.platform.startswith(('cygwin', 'win')):
739 subdir = 'win'
740 elif sys.platform == 'darwin':
741 subdir = 'mac'
742 elif sys.platform.startswith('linux'):
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000743 subdir = 'linux64'
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000744 else:
745 raise Error('Unknown platform: ' + sys.platform)
746 return os.path.join(buildtools_path, subdir)
747
748
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000749def GetExeSuffix():
750 """Returns '' or '.exe' depending on how executables work on this platform."""
751 if sys.platform.startswith(('cygwin', 'win')):
752 return '.exe'
753 return ''
754
755
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000756def GetGClientPrimarySolutionName(gclient_root_dir_path):
757 """Returns the name of the primary solution in the .gclient file specified."""
758 gclient_config_file = os.path.join(gclient_root_dir_path, '.gclient')
759 env = {}
760 execfile(gclient_config_file, env)
761 solutions = env.get('solutions', [])
762 if solutions:
763 return solutions[0].get('name')
764 return None
765
766
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000767def GetGClientRootAndEntries(path=None):
768 """Returns the gclient root and the dict of entries."""
769 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000770 root = FindFileUpwards(config_file, path)
771 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000772 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000773 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000774 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000775 env = {}
776 execfile(config_path, env)
777 config_dir = os.path.dirname(config_path)
778 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000779
780
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000781def lockedmethod(method):
782 """Method decorator that holds self.lock for the duration of the call."""
783 def inner(self, *args, **kwargs):
784 try:
785 try:
786 self.lock.acquire()
787 except KeyboardInterrupt:
788 print >> sys.stderr, 'Was deadlocked'
789 raise
790 return method(self, *args, **kwargs)
791 finally:
792 self.lock.release()
793 return inner
794
795
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000796class WorkItem(object):
797 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000798 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
799 # As a workaround, use a single lock. Yep you read it right. Single lock for
800 # all the 100 objects.
801 lock = threading.Lock()
802
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000803 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000804 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000805 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000806 self.outbuf = cStringIO.StringIO()
807 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700808 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000809
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000810 def run(self, work_queue):
811 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000812 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000813 pass
814
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000815 @property
816 def name(self):
817 return self._name
818
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000819
820class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000821 """Runs a set of WorkItem that have interdependencies and were WorkItem are
822 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000823
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200824 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000825 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000826
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000827 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000828 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000829 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000830 """jobs specifies the number of concurrent tasks to allow. progress is a
831 Progress instance."""
832 # Set when a thread is done or a new item is enqueued.
833 self.ready_cond = threading.Condition()
834 # Maximum number of concurrent tasks.
835 self.jobs = jobs
836 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000837 self.queued = []
838 # List of strings representing each Dependency.name that was run.
839 self.ran = []
840 # List of items currently running.
841 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000842 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000843 self.exceptions = Queue.Queue()
844 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000845 self.progress = progress
846 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000847 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000848
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000849 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000850 self.verbose = verbose
851 self.last_join = None
852 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000853
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000854 def enqueue(self, d):
855 """Enqueue one Dependency to be executed later once its requirements are
856 satisfied.
857 """
858 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000859 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000860 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000861 self.queued.append(d)
862 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000863 if self.jobs == 1:
864 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000865 logging.debug('enqueued(%s)' % d.name)
866 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000867 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000868 self.progress.update(0)
869 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000870 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000871 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000872
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000873 def out_cb(self, _):
874 self.last_subproc_output = datetime.datetime.now()
875 return True
876
877 @staticmethod
878 def format_task_output(task, comment=''):
879 if comment:
880 comment = ' (%s)' % comment
881 if task.start and task.finish:
882 elapsed = ' (Elapsed: %s)' % (
883 str(task.finish - task.start).partition('.')[0])
884 else:
885 elapsed = ''
886 return """
887%s%s%s
888----------------------------------------
889%s
890----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000891 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000892
hinoka885e5b12016-06-08 14:40:09 -0700893 def _is_conflict(self, job):
894 """Checks to see if a job will conflict with another running job."""
895 for running_job in self.running:
896 for used_resource in running_job.item.resources:
897 logging.debug('Checking resource %s' % used_resource)
898 if used_resource in job.resources:
899 return True
900 return False
901
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000902 def flush(self, *args, **kwargs):
903 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000904 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000905 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000906 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000907 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000908 while True:
909 # Check for task to run first, then wait.
910 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000911 if not self.exceptions.empty():
912 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000913 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000914 self._flush_terminated_threads()
915 if (not self.queued and not self.running or
916 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000917 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000918 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000919
920 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000921 for i in xrange(len(self.queued)):
922 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000923 if (self.ignore_requirements or
924 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700925 if not self._is_conflict(self.queued[i]):
926 # Start one work item: all its requirements are satisfied.
927 self._run_one_task(self.queued.pop(i), args, kwargs)
928 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000929 else:
930 # Couldn't find an item that could run. Break out the outher loop.
931 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000932
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000933 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000934 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000935 break
936 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000937 try:
938 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000939 # If we haven't printed to terminal for a while, but we have received
940 # spew from a suprocess, let the user know we're still progressing.
941 now = datetime.datetime.now()
942 if (now - self.last_join > datetime.timedelta(seconds=60) and
943 self.last_subproc_output > self.last_join):
944 if self.progress:
945 print >> sys.stdout, ''
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000946 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000947 elapsed = Elapsed()
948 print >> sys.stdout, '[%s] Still working on:' % elapsed
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000949 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000950 for task in self.running:
951 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000952 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000953 except KeyboardInterrupt:
954 # Help debugging by printing some information:
955 print >> sys.stderr, (
956 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
957 'Running: %d') % (
958 self.jobs,
959 len(self.queued),
960 ', '.join(self.ran),
961 len(self.running)))
962 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000963 print >> sys.stderr, '%s (not started): %s' % (
964 i.name, ', '.join(i.requirements))
965 for i in self.running:
966 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000967 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000968 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000969 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000970 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000971
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000972 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000973 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000974 if self.progress:
975 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000976 # To get back the stack location correctly, the raise a, b, c form must be
977 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000978 e, task = self.exceptions.get()
979 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000980 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000981 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000982 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000983
maruel@chromium.org3742c842010-09-09 19:27:14 +0000984 def _flush_terminated_threads(self):
985 """Flush threads that have terminated."""
986 running = self.running
987 self.running = []
988 for t in running:
989 if t.isAlive():
990 self.running.append(t)
991 else:
992 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000993 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000994 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000995 if self.verbose:
996 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000997 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000998 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000999 if t.item.name in self.ran:
1000 raise Error(
1001 'gclient is confused, "%s" is already in "%s"' % (
1002 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +00001003 if not t.item.name in self.ran:
1004 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001005
1006 def _run_one_task(self, task_item, args, kwargs):
1007 if self.jobs > 1:
1008 # Start the thread.
1009 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001010 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001011 self.running.append(new_thread)
1012 new_thread.start()
1013 else:
1014 # Run the 'thread' inside the main thread. Don't try to catch any
1015 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001016 try:
1017 task_item.start = datetime.datetime.now()
1018 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
1019 task_item.run(*args, **kwargs)
1020 task_item.finish = datetime.datetime.now()
1021 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
1022 self.ran.append(task_item.name)
1023 if self.verbose:
1024 if self.progress:
1025 print >> sys.stdout, ''
1026 print >> sys.stdout, self.format_task_output(task_item)
1027 if self.progress:
1028 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1029 except KeyboardInterrupt:
1030 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
1031 raise
1032 except Exception:
1033 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
1034 raise
1035
maruel@chromium.org3742c842010-09-09 19:27:14 +00001036
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001037 class _Worker(threading.Thread):
1038 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001039 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001040 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001041 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001042 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001043 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001044 self.args = args
1045 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001046 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001047
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001048 def run(self):
1049 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001050 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001051 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001052 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001053 self.item.start = datetime.datetime.now()
1054 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001055 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001056 self.item.finish = datetime.datetime.now()
1057 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001058 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001059 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001060 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001061 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001062 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001063 except Exception:
1064 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001065 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001066 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001067 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001068 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001069 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001070 work_queue.ready_cond.acquire()
1071 try:
1072 work_queue.ready_cond.notifyAll()
1073 finally:
1074 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001075
1076
agable92bec4f2016-08-24 09:27:27 -07001077def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001078 """Returns the most plausible editor to use.
1079
1080 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001081 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001082 - core.editor git configuration variable (if supplied by git-cl)
1083 - VISUAL environment variable
1084 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001085 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001086
1087 In the case of git-cl, this matches git's behaviour, except that it does not
1088 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001089 """
agable92bec4f2016-08-24 09:27:27 -07001090 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001091 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001092 editor = os.environ.get('VISUAL')
1093 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001094 editor = os.environ.get('EDITOR')
1095 if not editor:
1096 if sys.platform.startswith('win'):
1097 editor = 'notepad'
1098 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001099 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001100 return editor
1101
1102
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001103def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001104 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001105 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001106 # Make sure CRLF is handled properly by requiring none.
1107 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +00001108 print >> sys.stderr, (
1109 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001110 fileobj = os.fdopen(file_handle, 'w')
1111 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001112 content = re.sub('\r?\n', '\n', content)
1113 # Some editors complain when the file doesn't end in \n.
1114 if not content.endswith('\n'):
1115 content += '\n'
1116 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001117 fileobj.close()
1118
1119 try:
agable92bec4f2016-08-24 09:27:27 -07001120 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001121 if not editor:
1122 return None
1123 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001124 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1125 # Msysgit requires the usage of 'env' to be present.
1126 cmd = 'env ' + cmd
1127 try:
1128 # shell=True to allow the shell to handle all forms of quotes in
1129 # $EDITOR.
1130 subprocess2.check_call(cmd, shell=True)
1131 except subprocess2.CalledProcessError:
1132 return None
1133 return FileRead(filename)
1134 finally:
1135 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001136
1137
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001138def UpgradeToHttps(url):
1139 """Upgrades random urls to https://.
1140
1141 Do not touch unknown urls like ssh:// or git://.
1142 Do not touch http:// urls with a port number,
1143 Fixes invalid GAE url.
1144 """
1145 if not url:
1146 return url
1147 if not re.match(r'[a-z\-]+\://.*', url):
1148 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1149 # relative url and will use http:///foo. Note that it defaults to http://
1150 # for compatibility with naked url like "localhost:8080".
1151 url = 'http://%s' % url
1152 parsed = list(urlparse.urlparse(url))
1153 # Do not automatically upgrade http to https if a port number is provided.
1154 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1155 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001156 return urlparse.urlunparse(parsed)
1157
1158
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001159def ParseCodereviewSettingsContent(content):
1160 """Process a codereview.settings file properly."""
1161 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1162 try:
1163 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1164 except ValueError:
1165 raise Error(
1166 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001167 def fix_url(key):
1168 if keyvals.get(key):
1169 keyvals[key] = UpgradeToHttps(keyvals[key])
1170 fix_url('CODE_REVIEW_SERVER')
1171 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001172 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001173
1174
1175def NumLocalCpus():
1176 """Returns the number of processors.
1177
dnj@chromium.org530523b2015-01-07 19:54:57 +00001178 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1179 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1180 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001181 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001182 # Surround the entire thing in try/except; no failure here should stop gclient
1183 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001184 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001185 # Use multiprocessing to get CPU count. This may raise
1186 # NotImplementedError.
1187 try:
1188 import multiprocessing
1189 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001190 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001191 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001192 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001193 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1194 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1195
1196 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1197 if 'NUMBER_OF_PROCESSORS' in os.environ:
1198 return int(os.environ['NUMBER_OF_PROCESSORS'])
1199 except Exception as e:
1200 logging.exception("Exception raised while probing CPU count: %s", e)
1201
1202 logging.debug('Failed to get CPU count. Defaulting to 1.')
1203 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001204
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001205
szager@chromium.orgfc616382014-03-18 20:32:04 +00001206def DefaultDeltaBaseCacheLimit():
1207 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1208
1209 The primary constraint is the address space of virtual memory. The cache
1210 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1211 parameter is set too high.
1212 """
1213 if platform.architecture()[0].startswith('64'):
1214 return '2g'
1215 else:
1216 return '512m'
1217
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001218
szager@chromium.orgff113292014-03-25 06:02:08 +00001219def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001220 """Return reasonable default values for configuring git-index-pack.
1221
1222 Experiments suggest that higher values for pack.threads don't improve
1223 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001224 cache_limit = DefaultDeltaBaseCacheLimit()
1225 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1226 if url in THREADED_INDEX_PACK_BLACKLIST:
1227 result.extend(['-c', 'pack.threads=1'])
1228 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001229
1230
1231def FindExecutable(executable):
1232 """This mimics the "which" utility."""
1233 path_folders = os.environ.get('PATH').split(os.pathsep)
1234
1235 for path_folder in path_folders:
1236 target = os.path.join(path_folder, executable)
1237 # Just incase we have some ~/blah paths.
1238 target = os.path.abspath(os.path.expanduser(target))
1239 if os.path.isfile(target) and os.access(target, os.X_OK):
1240 return target
1241 if sys.platform.startswith('win'):
1242 for suffix in ('.bat', '.cmd', '.exe'):
1243 alt_target = target + suffix
1244 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1245 return alt_target
1246 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001247
1248
1249def freeze(obj):
1250 """Takes a generic object ``obj``, and returns an immutable version of it.
1251
1252 Supported types:
1253 * dict / OrderedDict -> FrozenDict
1254 * list -> tuple
1255 * set -> frozenset
1256 * any object with a working __hash__ implementation (assumes that hashable
1257 means immutable)
1258
1259 Will raise TypeError if you pass an object which is not hashable.
1260 """
1261 if isinstance(obj, dict):
1262 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.iteritems())
1263 elif isinstance(obj, (list, tuple)):
1264 return tuple(freeze(i) for i in obj)
1265 elif isinstance(obj, set):
1266 return frozenset(freeze(i) for i in obj)
1267 else:
1268 hash(obj)
1269 return obj
1270
1271
1272class FrozenDict(collections.Mapping):
1273 """An immutable OrderedDict.
1274
1275 Modified From: http://stackoverflow.com/a/2704866
1276 """
1277 def __init__(self, *args, **kwargs):
1278 self._d = collections.OrderedDict(*args, **kwargs)
1279
1280 # Calculate the hash immediately so that we know all the items are
1281 # hashable too.
1282 self._hash = reduce(operator.xor,
1283 (hash(i) for i in enumerate(self._d.iteritems())), 0)
1284
1285 def __eq__(self, other):
1286 if not isinstance(other, collections.Mapping):
1287 return NotImplemented
1288 if self is other:
1289 return True
1290 if len(self) != len(other):
1291 return False
1292 for k, v in self.iteritems():
1293 if k not in other or other[k] != v:
1294 return False
1295 return True
1296
1297 def __iter__(self):
1298 return iter(self._d)
1299
1300 def __len__(self):
1301 return len(self._d)
1302
1303 def __getitem__(self, key):
1304 return self._d[key]
1305
1306 def __hash__(self):
1307 return self._hash
1308
1309 def __repr__(self):
1310 return 'FrozenDict(%r)' % (self._d.items(),)