blob: 30306a7cd07482ac79e36c667420e61d59d9dbc7 [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
floitsch@google.comeaab7842011-04-28 09:07:58 +000095def IsDateRevision(revision):
96 """Returns true if the given revision is of the form "{ ... }"."""
97 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
98
99
100def MakeDateRevision(date):
101 """Returns a revision representing the latest revision before the given
102 date."""
103 return "{" + date + "}"
104
105
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000106def SyntaxErrorToError(filename, e):
107 """Raises a gclient_utils.Error exception with the human readable message"""
108 try:
109 # Try to construct a human readable error message
110 if filename:
111 error_message = 'There is a syntax error in %s\n' % filename
112 else:
113 error_message = 'There is a syntax error\n'
114 error_message += 'Line #%s, character %s: "%s"' % (
115 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
116 except:
117 # Something went wrong, re-raise the original exception
118 raise e
119 else:
120 raise Error(error_message)
121
122
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000123class PrintableObject(object):
124 def __str__(self):
125 output = ''
126 for i in dir(self):
127 if i.startswith('__'):
128 continue
129 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
130 return output
131
132
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000133def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000134 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000135 # codecs.open() has different behavior than open() on python 2.6 so use
136 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000137 s = f.read()
138 try:
139 return s.decode('utf-8')
140 except UnicodeDecodeError:
141 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000142
143
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000144def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000145 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000146 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000147
148
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000149@contextlib.contextmanager
150def temporary_directory(**kwargs):
151 tdir = tempfile.mkdtemp(**kwargs)
152 try:
153 yield tdir
154 finally:
155 if tdir:
156 rmtree(tdir)
157
158
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000159def safe_rename(old, new):
160 """Renames a file reliably.
161
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000162 Sometimes os.rename does not work because a dying git process keeps a handle
163 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000164 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000165 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000166 """
167 # roughly 10s
168 retries = 100
169 for i in range(retries):
170 try:
171 os.rename(old, new)
172 break
173 except OSError:
174 if i == (retries - 1):
175 # Give up.
176 raise
177 # retry
178 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
179 time.sleep(0.1)
180
181
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000182def rm_file_or_tree(path):
183 if os.path.isfile(path):
184 os.remove(path)
185 else:
186 rmtree(path)
187
188
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000189def rmtree(path):
190 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000191
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000192 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000193
194 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700195 are read-only. We need to be able to force the files to be writable (i.e.,
196 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000197
198 Even with all this, Windows still sometimes fails to delete a file, citing
199 a permission error (maybe something to do with antivirus scans or disk
200 indexing). The best suggestion any of the user forums had was to wait a
201 bit and try again, so we do that too. It's hand-waving, but sometimes it
202 works. :/
203
204 On POSIX systems, things are a little bit simpler. The modes of the files
205 to be deleted doesn't matter, only the modes of the directories containing
206 them are significant. As the directory tree is traversed, each directory
207 has its mode set appropriately before descending into it. This should
208 result in the entire tree being removed, with the possible exception of
209 *path itself, because nothing attempts to change the mode of its parent.
210 Doing so would be hazardous, as it's not a directory slated for removal.
211 In the ordinary case, this is not a problem: for our purposes, the user
212 will never lack write permission on *path's parent.
213 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000214 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000215 return
216
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000217 if os.path.islink(path) or not os.path.isdir(path):
218 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000219
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000221 # Give up and use cmd.exe's rd command.
222 path = os.path.normcase(path)
223 for _ in xrange(3):
224 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
225 if exitcode == 0:
226 return
227 else:
228 print >> sys.stderr, 'rd exited with code %d' % exitcode
229 time.sleep(3)
230 raise Exception('Failed to remove path %s' % path)
231
232 # On POSIX systems, we need the x-bit set on the directory to access it,
233 # the r-bit to see its contents, and the w-bit to remove files from it.
234 # The actual modes of the files within the directory is irrelevant.
235 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000236
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000237 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000238 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000239
240 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000241 # If fullpath is a symbolic link that points to a directory, isdir will
242 # be True, but we don't want to descend into that as a directory, we just
243 # want to remove the link. Check islink and treat links as ordinary files
244 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000245 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000246 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000247 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000248 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000249 # Recurse.
250 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000251
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000252 remove(os.rmdir, path)
253
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000254
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000255def safe_makedirs(tree):
256 """Creates the directory in a safe manner.
257
258 Because multiple threads can create these directories concurently, trap the
259 exception and pass on.
260 """
261 count = 0
262 while not os.path.exists(tree):
263 count += 1
264 try:
265 os.makedirs(tree)
266 except OSError, e:
267 # 17 POSIX, 183 Windows
268 if e.errno not in (17, 183):
269 raise
270 if count > 40:
271 # Give up.
272 raise
273
274
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000275def CommandToStr(args):
276 """Converts an arg list into a shell escaped string."""
277 return ' '.join(pipes.quote(arg) for arg in args)
278
279
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000280def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000281 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000282
maruel@chromium.org17d01792010-09-01 18:07:10 +0000283 If |always| is True, a message indicating what is being done
284 is printed to stdout all the time even if not output is generated. Otherwise
285 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000286 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000287 stdout = kwargs.setdefault('stdout', sys.stdout)
288 if header is None:
289 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000290 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000291
maruel@chromium.org17d01792010-09-01 18:07:10 +0000292 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000293 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000294 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000295 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000296 def filter_msg(line):
297 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000298 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000299 elif filter_fn:
300 filter_fn(line)
301 kwargs['filter_fn'] = filter_msg
302 kwargs['call_filter_on_first_line'] = True
303 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000304 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000305 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000306
maruel@chromium.org17d01792010-09-01 18:07:10 +0000307
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000308class Wrapper(object):
309 """Wraps an object, acting as a transparent proxy for all properties by
310 default.
311 """
312 def __init__(self, wrapped):
313 self._wrapped = wrapped
314
315 def __getattr__(self, name):
316 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000317
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000318
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000319class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000320 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000321 def __init__(self, wrapped, delay):
322 super(AutoFlush, self).__init__(wrapped)
323 if not hasattr(self, 'lock'):
324 self.lock = threading.Lock()
325 self.__last_flushed_at = time.time()
326 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000327
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000328 @property
329 def autoflush(self):
330 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000331
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000332 def write(self, out, *args, **kwargs):
333 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000334 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000335 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000336 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000337 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000338 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000339 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000340 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000341 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000342 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000343 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000344
345
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000346class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000347 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000348 threads with a NN> prefix.
349 """
350 def __init__(self, wrapped, include_zero=False):
351 super(Annotated, self).__init__(wrapped)
352 if not hasattr(self, 'lock'):
353 self.lock = threading.Lock()
354 self.__output_buffers = {}
355 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000356
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000357 @property
358 def annotated(self):
359 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000360
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000361 def write(self, out):
362 index = getattr(threading.currentThread(), 'index', 0)
363 if not index and not self.__include_zero:
364 # Unindexed threads aren't buffered.
365 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000366
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000367 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000368 try:
369 # Use a dummy array to hold the string so the code can be lockless.
370 # Strings are immutable, requiring to keep a lock for the whole dictionary
371 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000372 if not index in self.__output_buffers:
373 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000374 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000375 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000376 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000377 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000378
379 # Continue lockless.
380 obj[0] += out
381 while '\n' in obj[0]:
382 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000383 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000384 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000385 obj[0] = remaining
386
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000388 """Flush buffered output."""
389 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000390 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000391 try:
392 # Detect threads no longer existing.
393 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000394 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000395 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000396 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000397 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000398 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000399 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000400 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000401 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000402
403 # Don't keep the lock while writting. Will append \n when it shouldn't.
404 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000405 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000406 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
407 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000408
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000409
410def MakeFileAutoFlush(fileobj, delay=10):
411 autoflush = getattr(fileobj, 'autoflush', None)
412 if autoflush:
413 autoflush.delay = delay
414 return fileobj
415 return AutoFlush(fileobj, delay)
416
417
418def MakeFileAnnotated(fileobj, include_zero=False):
419 if getattr(fileobj, 'annotated', None):
420 return fileobj
421 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000422
423
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000424GCLIENT_CHILDREN = []
425GCLIENT_CHILDREN_LOCK = threading.Lock()
426
427
428class GClientChildren(object):
429 @staticmethod
430 def add(popen_obj):
431 with GCLIENT_CHILDREN_LOCK:
432 GCLIENT_CHILDREN.append(popen_obj)
433
434 @staticmethod
435 def remove(popen_obj):
436 with GCLIENT_CHILDREN_LOCK:
437 GCLIENT_CHILDREN.remove(popen_obj)
438
439 @staticmethod
440 def _attemptToKillChildren():
441 global GCLIENT_CHILDREN
442 with GCLIENT_CHILDREN_LOCK:
443 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
444
445 for zombie in zombies:
446 try:
447 zombie.kill()
448 except OSError:
449 pass
450
451 with GCLIENT_CHILDREN_LOCK:
452 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
453
454 @staticmethod
455 def _areZombies():
456 with GCLIENT_CHILDREN_LOCK:
457 return bool(GCLIENT_CHILDREN)
458
459 @staticmethod
460 def KillAllRemainingChildren():
461 GClientChildren._attemptToKillChildren()
462
463 if GClientChildren._areZombies():
464 time.sleep(0.5)
465 GClientChildren._attemptToKillChildren()
466
467 with GCLIENT_CHILDREN_LOCK:
468 if GCLIENT_CHILDREN:
469 print >> sys.stderr, 'Could not kill the following subprocesses:'
470 for zombie in GCLIENT_CHILDREN:
471 print >> sys.stderr, ' ', zombie.pid
472
473
maruel@chromium.org17d01792010-09-01 18:07:10 +0000474def CheckCallAndFilter(args, stdout=None, filter_fn=None,
475 print_stdout=None, call_filter_on_first_line=False,
tandrii64103db2016-10-11 05:30:05 -0700476 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000477 """Runs a command and calls back a filter function if needed.
478
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000479 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000480 print_stdout: If True, the command's stdout is forwarded to stdout.
481 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000482 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000483 character trimmed.
484 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000485 retry: If the process exits non-zero, sleep for a brief interval and try
486 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000487
488 stderr is always redirected to stdout.
489 """
490 assert print_stdout or filter_fn
491 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000492 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000493 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000494
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000495 sleep_interval = RETRY_INITIAL_SLEEP
496 run_cwd = kwargs.get('cwd', os.getcwd())
497 for _ in xrange(RETRY_MAX + 1):
498 kid = subprocess2.Popen(
499 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
500 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000501
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000502 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000503
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000504 # Do a flush of stdout before we begin reading from the subprocess2's stdout
505 stdout.flush()
506
507 # Also, we need to forward stdout to prevent weird re-ordering of output.
508 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700509 # normally buffering is done for each line, but if the process requests
510 # input, no end-of-line character is output after the prompt and it would
511 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000512 try:
513 in_byte = kid.stdout.read(1)
514 if in_byte:
515 if call_filter_on_first_line:
516 filter_fn(None)
517 in_line = ''
518 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000519 output.write(in_byte)
520 if print_stdout:
521 stdout.write(in_byte)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000522 if in_byte not in ['\r', '\n']:
523 in_line += in_byte
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000524 else:
525 filter_fn(in_line)
526 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000527 in_byte = kid.stdout.read(1)
528 # Flush the rest of buffered output. This is only an issue with
529 # stdout/stderr not ending with a \n.
530 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000531 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000532 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000533
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000534 # Don't put this in a 'finally,' since the child may still run if we get
535 # an exception.
536 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000537
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000538 except KeyboardInterrupt:
539 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
540 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000541
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000542 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000543 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000544 if not retry:
545 break
tandrii30d95622016-10-11 05:20:26 -0700546 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
547 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000548 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000549 sleep_interval *= 2
550 raise subprocess2.CalledProcessError(
551 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000552
553
agable@chromium.org5a306a22014-02-24 22:13:59 +0000554class GitFilter(object):
555 """A filter_fn implementation for quieting down git output messages.
556
557 Allows a custom function to skip certain lines (predicate), and will throttle
558 the output of percentage completed lines to only output every X seconds.
559 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000560 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000561
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000562 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000563 """
564 Args:
565 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
566 XX% complete messages) to only be printed at least |time_throttle|
567 seconds apart.
568 predicate (f(line)): An optional function which is invoked for every line.
569 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000570 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000571 """
572 self.last_time = 0
573 self.time_throttle = time_throttle
574 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000575 self.out_fh = out_fh or sys.stdout
576 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000577
578 def __call__(self, line):
579 # git uses an escape sequence to clear the line; elide it.
580 esc = line.find(unichr(033))
581 if esc > -1:
582 line = line[:esc]
583 if self.predicate and not self.predicate(line):
584 return
585 now = time.time()
586 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000587 if match:
588 if match.group(1) != self.progress_prefix:
589 self.progress_prefix = match.group(1)
590 elif now - self.last_time < self.time_throttle:
591 return
592 self.last_time = now
593 self.out_fh.write('[%s] ' % Elapsed())
594 print >> self.out_fh, line
agable@chromium.org5a306a22014-02-24 22:13:59 +0000595
596
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000597def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000598 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000599 real_from_dir = os.path.realpath(from_dir)
600 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000601 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000602 split_path = os.path.split(path)
603 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000604 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000605 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000606
607 # If we did not find the file in the current directory, make sure we are in a
608 # sub directory that is controlled by this configuration.
609 if path != real_from_dir:
610 entries_filename = os.path.join(path, filename + '_entries')
611 if not os.path.exists(entries_filename):
612 # If .gclient_entries does not exist, a previous call to gclient sync
613 # might have failed. In that case, we cannot verify that the .gclient
614 # is the one we want to use. In order to not to cause too much trouble,
615 # just issue a warning and return the path anyway.
Bruce Dawson1c5c1182017-06-13 10:34:06 -0700616 print >> sys.stderr, ("%s missing, %s file in parent directory %s might "
617 "not be the file you want to use." %
618 (entries_filename, filename, path))
jochen@chromium.org20760a52010-09-08 08:47:28 +0000619 return path
620 scope = {}
621 try:
622 exec(FileRead(entries_filename), scope)
623 except SyntaxError, e:
624 SyntaxErrorToError(filename, e)
625 all_directories = scope['entries'].keys()
626 path_to_check = real_from_dir[len(path)+1:]
627 while path_to_check:
628 if path_to_check in all_directories:
629 return path
630 path_to_check = os.path.dirname(path_to_check)
631 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000632
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000633 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000634 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000635
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000636
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000637def PathDifference(root, subpath):
638 """Returns the difference subpath minus root."""
639 root = os.path.realpath(root)
640 subpath = os.path.realpath(subpath)
641 if not subpath.startswith(root):
642 return None
643 # If the root does not have a trailing \ or /, we add it so the returned
644 # path starts immediately after the seperator regardless of whether it is
645 # provided.
646 root = os.path.join(root, '')
647 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000648
649
650def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000651 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000652
rcui@google.com13595ff2011-10-13 01:25:07 +0000653 Returns nearest upper-level directory with the passed in file.
654 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000655 if not path:
656 path = os.getcwd()
657 path = os.path.realpath(path)
658 while True:
659 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000660 if os.path.exists(file_path):
661 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000662 (new_path, _) = os.path.split(path)
663 if new_path == path:
664 return None
665 path = new_path
666
667
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000668def GetMacWinOrLinux():
669 """Returns 'mac', 'win', or 'linux', matching the current platform."""
670 if sys.platform.startswith(('cygwin', 'win')):
671 return 'win'
672 elif sys.platform.startswith('linux'):
673 return 'linux'
674 elif sys.platform == 'darwin':
675 return 'mac'
676 raise Error('Unknown platform: ' + sys.platform)
677
678
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000679def GetPrimarySolutionPath():
680 """Returns the full path to the primary solution. (gclient_root + src)"""
zturner@chromium.org0db9a142014-08-13 23:15:25 +0000681
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000682 gclient_root = FindGclientRoot(os.getcwd())
683 if not gclient_root:
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000684 # Some projects might not use .gclient. Try to see whether we're in a git
685 # checkout.
686 top_dir = [os.getcwd()]
687 def filter_fn(line):
hanpfei13f9c372016-08-08 22:05:56 -0700688 repo_root_path = os.path.normpath(line.rstrip('\n'))
689 if os.path.exists(repo_root_path):
690 top_dir[0] = repo_root_path
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000691 try:
692 CheckCallAndFilter(["git", "rev-parse", "--show-toplevel"],
693 print_stdout=False, filter_fn=filter_fn)
694 except Exception:
695 pass
696 top_dir = top_dir[0]
697 if os.path.exists(os.path.join(top_dir, 'buildtools')):
jiangj@opera.comd6d15b82015-04-20 06:43:48 +0000698 return top_dir
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000699 return None
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000700
701 # Some projects' top directory is not named 'src'.
702 source_dir_name = GetGClientPrimarySolutionName(gclient_root) or 'src'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000703 return os.path.join(gclient_root, source_dir_name)
704
705
706def GetBuildtoolsPath():
707 """Returns the full path to the buildtools directory.
708 This is based on the root of the checkout containing the current directory."""
709
710 # Overriding the build tools path by environment is highly unsupported and may
711 # break without warning. Do not rely on this for anything important.
712 override = os.environ.get('CHROMIUM_BUILDTOOLS_PATH')
713 if override is not None:
714 return override
715
716 primary_solution = GetPrimarySolutionPath()
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000717 if not primary_solution:
718 return None
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000719 buildtools_path = os.path.join(primary_solution, 'buildtools')
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000720 if not os.path.exists(buildtools_path):
721 # Buildtools may be in the gclient root.
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000722 gclient_root = FindGclientRoot(os.getcwd())
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000723 buildtools_path = os.path.join(gclient_root, 'buildtools')
724 return buildtools_path
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000725
726
727def GetBuildtoolsPlatformBinaryPath():
728 """Returns the full path to the binary directory for the current platform."""
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000729 buildtools_path = GetBuildtoolsPath()
730 if not buildtools_path:
731 return None
732
733 if sys.platform.startswith(('cygwin', 'win')):
734 subdir = 'win'
735 elif sys.platform == 'darwin':
736 subdir = 'mac'
737 elif sys.platform.startswith('linux'):
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000738 subdir = 'linux64'
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000739 else:
740 raise Error('Unknown platform: ' + sys.platform)
741 return os.path.join(buildtools_path, subdir)
742
743
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000744def GetExeSuffix():
745 """Returns '' or '.exe' depending on how executables work on this platform."""
746 if sys.platform.startswith(('cygwin', 'win')):
747 return '.exe'
748 return ''
749
750
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000751def GetGClientPrimarySolutionName(gclient_root_dir_path):
752 """Returns the name of the primary solution in the .gclient file specified."""
753 gclient_config_file = os.path.join(gclient_root_dir_path, '.gclient')
754 env = {}
755 execfile(gclient_config_file, env)
756 solutions = env.get('solutions', [])
757 if solutions:
758 return solutions[0].get('name')
759 return None
760
761
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000762def GetGClientRootAndEntries(path=None):
763 """Returns the gclient root and the dict of entries."""
764 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000765 root = FindFileUpwards(config_file, path)
766 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000767 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000768 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000769 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000770 env = {}
771 execfile(config_path, env)
772 config_dir = os.path.dirname(config_path)
773 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000774
775
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000776def lockedmethod(method):
777 """Method decorator that holds self.lock for the duration of the call."""
778 def inner(self, *args, **kwargs):
779 try:
780 try:
781 self.lock.acquire()
782 except KeyboardInterrupt:
783 print >> sys.stderr, 'Was deadlocked'
784 raise
785 return method(self, *args, **kwargs)
786 finally:
787 self.lock.release()
788 return inner
789
790
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000791class WorkItem(object):
792 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000793 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
794 # As a workaround, use a single lock. Yep you read it right. Single lock for
795 # all the 100 objects.
796 lock = threading.Lock()
797
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000798 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000799 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000800 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000801 self.outbuf = cStringIO.StringIO()
802 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700803 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000804
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000805 def run(self, work_queue):
806 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000807 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000808 pass
809
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000810 @property
811 def name(self):
812 return self._name
813
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000814
815class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000816 """Runs a set of WorkItem that have interdependencies and were WorkItem are
817 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000818
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200819 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000820 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000821
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000822 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000823 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000824 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000825 """jobs specifies the number of concurrent tasks to allow. progress is a
826 Progress instance."""
827 # Set when a thread is done or a new item is enqueued.
828 self.ready_cond = threading.Condition()
829 # Maximum number of concurrent tasks.
830 self.jobs = jobs
831 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000832 self.queued = []
833 # List of strings representing each Dependency.name that was run.
834 self.ran = []
835 # List of items currently running.
836 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000837 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000838 self.exceptions = Queue.Queue()
839 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000840 self.progress = progress
841 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000842 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000843
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000844 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000845 self.verbose = verbose
846 self.last_join = None
847 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000848
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000849 def enqueue(self, d):
850 """Enqueue one Dependency to be executed later once its requirements are
851 satisfied.
852 """
853 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000854 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000855 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000856 self.queued.append(d)
857 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000858 if self.jobs == 1:
859 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000860 logging.debug('enqueued(%s)' % d.name)
861 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000862 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000863 self.progress.update(0)
864 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000865 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000866 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000867
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000868 def out_cb(self, _):
869 self.last_subproc_output = datetime.datetime.now()
870 return True
871
872 @staticmethod
873 def format_task_output(task, comment=''):
874 if comment:
875 comment = ' (%s)' % comment
876 if task.start and task.finish:
877 elapsed = ' (Elapsed: %s)' % (
878 str(task.finish - task.start).partition('.')[0])
879 else:
880 elapsed = ''
881 return """
882%s%s%s
883----------------------------------------
884%s
885----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000886 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000887
hinoka885e5b12016-06-08 14:40:09 -0700888 def _is_conflict(self, job):
889 """Checks to see if a job will conflict with another running job."""
890 for running_job in self.running:
891 for used_resource in running_job.item.resources:
892 logging.debug('Checking resource %s' % used_resource)
893 if used_resource in job.resources:
894 return True
895 return False
896
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000897 def flush(self, *args, **kwargs):
898 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000899 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000900 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000901 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000902 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000903 while True:
904 # Check for task to run first, then wait.
905 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000906 if not self.exceptions.empty():
907 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000908 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000909 self._flush_terminated_threads()
910 if (not self.queued and not self.running or
911 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000912 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000913 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000914
915 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000916 for i in xrange(len(self.queued)):
917 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000918 if (self.ignore_requirements or
919 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700920 if not self._is_conflict(self.queued[i]):
921 # Start one work item: all its requirements are satisfied.
922 self._run_one_task(self.queued.pop(i), args, kwargs)
923 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000924 else:
925 # Couldn't find an item that could run. Break out the outher loop.
926 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000927
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000928 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000929 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000930 break
931 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000932 try:
933 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000934 # If we haven't printed to terminal for a while, but we have received
935 # spew from a suprocess, let the user know we're still progressing.
936 now = datetime.datetime.now()
937 if (now - self.last_join > datetime.timedelta(seconds=60) and
938 self.last_subproc_output > self.last_join):
939 if self.progress:
940 print >> sys.stdout, ''
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000941 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000942 elapsed = Elapsed()
943 print >> sys.stdout, '[%s] Still working on:' % elapsed
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000944 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000945 for task in self.running:
946 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000947 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000948 except KeyboardInterrupt:
949 # Help debugging by printing some information:
950 print >> sys.stderr, (
951 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
952 'Running: %d') % (
953 self.jobs,
954 len(self.queued),
955 ', '.join(self.ran),
956 len(self.running)))
957 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000958 print >> sys.stderr, '%s (not started): %s' % (
959 i.name, ', '.join(i.requirements))
960 for i in self.running:
961 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000962 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000963 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000964 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000965 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000966
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000967 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000968 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000969 if self.progress:
970 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000971 # To get back the stack location correctly, the raise a, b, c form must be
972 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000973 e, task = self.exceptions.get()
974 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000975 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000976 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000977 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000978
maruel@chromium.org3742c842010-09-09 19:27:14 +0000979 def _flush_terminated_threads(self):
980 """Flush threads that have terminated."""
981 running = self.running
982 self.running = []
983 for t in running:
984 if t.isAlive():
985 self.running.append(t)
986 else:
987 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000988 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000989 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000990 if self.verbose:
991 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000992 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000993 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000994 if t.item.name in self.ran:
995 raise Error(
996 'gclient is confused, "%s" is already in "%s"' % (
997 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000998 if not t.item.name in self.ran:
999 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001000
1001 def _run_one_task(self, task_item, args, kwargs):
1002 if self.jobs > 1:
1003 # Start the thread.
1004 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001005 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001006 self.running.append(new_thread)
1007 new_thread.start()
1008 else:
1009 # Run the 'thread' inside the main thread. Don't try to catch any
1010 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001011 try:
1012 task_item.start = datetime.datetime.now()
1013 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
1014 task_item.run(*args, **kwargs)
1015 task_item.finish = datetime.datetime.now()
1016 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
1017 self.ran.append(task_item.name)
1018 if self.verbose:
1019 if self.progress:
1020 print >> sys.stdout, ''
1021 print >> sys.stdout, self.format_task_output(task_item)
1022 if self.progress:
1023 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1024 except KeyboardInterrupt:
1025 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
1026 raise
1027 except Exception:
1028 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
1029 raise
1030
maruel@chromium.org3742c842010-09-09 19:27:14 +00001031
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001032 class _Worker(threading.Thread):
1033 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001034 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001035 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001036 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001037 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001038 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001039 self.args = args
1040 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001041 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001042
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001043 def run(self):
1044 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001045 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001046 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001047 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001048 self.item.start = datetime.datetime.now()
1049 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001050 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001051 self.item.finish = datetime.datetime.now()
1052 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001053 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001054 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001055 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001056 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001057 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001058 except Exception:
1059 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001060 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001061 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001062 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001063 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001064 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001065 work_queue.ready_cond.acquire()
1066 try:
1067 work_queue.ready_cond.notifyAll()
1068 finally:
1069 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001070
1071
agable92bec4f2016-08-24 09:27:27 -07001072def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001073 """Returns the most plausible editor to use.
1074
1075 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001076 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001077 - core.editor git configuration variable (if supplied by git-cl)
1078 - VISUAL environment variable
1079 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001080 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001081
1082 In the case of git-cl, this matches git's behaviour, except that it does not
1083 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001084 """
agable92bec4f2016-08-24 09:27:27 -07001085 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001086 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001087 editor = os.environ.get('VISUAL')
1088 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001089 editor = os.environ.get('EDITOR')
1090 if not editor:
1091 if sys.platform.startswith('win'):
1092 editor = 'notepad'
1093 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001094 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001095 return editor
1096
1097
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001098def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001099 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001100 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001101 # Make sure CRLF is handled properly by requiring none.
1102 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +00001103 print >> sys.stderr, (
1104 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001105 fileobj = os.fdopen(file_handle, 'w')
1106 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001107 content = re.sub('\r?\n', '\n', content)
1108 # Some editors complain when the file doesn't end in \n.
1109 if not content.endswith('\n'):
1110 content += '\n'
1111 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001112 fileobj.close()
1113
1114 try:
agable92bec4f2016-08-24 09:27:27 -07001115 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001116 if not editor:
1117 return None
1118 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001119 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1120 # Msysgit requires the usage of 'env' to be present.
1121 cmd = 'env ' + cmd
1122 try:
1123 # shell=True to allow the shell to handle all forms of quotes in
1124 # $EDITOR.
1125 subprocess2.check_call(cmd, shell=True)
1126 except subprocess2.CalledProcessError:
1127 return None
1128 return FileRead(filename)
1129 finally:
1130 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001131
1132
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001133def UpgradeToHttps(url):
1134 """Upgrades random urls to https://.
1135
1136 Do not touch unknown urls like ssh:// or git://.
1137 Do not touch http:// urls with a port number,
1138 Fixes invalid GAE url.
1139 """
1140 if not url:
1141 return url
1142 if not re.match(r'[a-z\-]+\://.*', url):
1143 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1144 # relative url and will use http:///foo. Note that it defaults to http://
1145 # for compatibility with naked url like "localhost:8080".
1146 url = 'http://%s' % url
1147 parsed = list(urlparse.urlparse(url))
1148 # Do not automatically upgrade http to https if a port number is provided.
1149 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1150 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001151 return urlparse.urlunparse(parsed)
1152
1153
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001154def ParseCodereviewSettingsContent(content):
1155 """Process a codereview.settings file properly."""
1156 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1157 try:
1158 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1159 except ValueError:
1160 raise Error(
1161 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001162 def fix_url(key):
1163 if keyvals.get(key):
1164 keyvals[key] = UpgradeToHttps(keyvals[key])
1165 fix_url('CODE_REVIEW_SERVER')
1166 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001167 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001168
1169
1170def NumLocalCpus():
1171 """Returns the number of processors.
1172
dnj@chromium.org530523b2015-01-07 19:54:57 +00001173 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1174 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1175 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001176 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001177 # Surround the entire thing in try/except; no failure here should stop gclient
1178 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001179 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001180 # Use multiprocessing to get CPU count. This may raise
1181 # NotImplementedError.
1182 try:
1183 import multiprocessing
1184 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001185 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001186 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001187 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001188 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1189 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1190
1191 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1192 if 'NUMBER_OF_PROCESSORS' in os.environ:
1193 return int(os.environ['NUMBER_OF_PROCESSORS'])
1194 except Exception as e:
1195 logging.exception("Exception raised while probing CPU count: %s", e)
1196
1197 logging.debug('Failed to get CPU count. Defaulting to 1.')
1198 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001199
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001200
szager@chromium.orgfc616382014-03-18 20:32:04 +00001201def DefaultDeltaBaseCacheLimit():
1202 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1203
1204 The primary constraint is the address space of virtual memory. The cache
1205 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1206 parameter is set too high.
1207 """
1208 if platform.architecture()[0].startswith('64'):
1209 return '2g'
1210 else:
1211 return '512m'
1212
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001213
szager@chromium.orgff113292014-03-25 06:02:08 +00001214def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001215 """Return reasonable default values for configuring git-index-pack.
1216
1217 Experiments suggest that higher values for pack.threads don't improve
1218 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001219 cache_limit = DefaultDeltaBaseCacheLimit()
1220 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1221 if url in THREADED_INDEX_PACK_BLACKLIST:
1222 result.extend(['-c', 'pack.threads=1'])
1223 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001224
1225
1226def FindExecutable(executable):
1227 """This mimics the "which" utility."""
1228 path_folders = os.environ.get('PATH').split(os.pathsep)
1229
1230 for path_folder in path_folders:
1231 target = os.path.join(path_folder, executable)
1232 # Just incase we have some ~/blah paths.
1233 target = os.path.abspath(os.path.expanduser(target))
1234 if os.path.isfile(target) and os.access(target, os.X_OK):
1235 return target
1236 if sys.platform.startswith('win'):
1237 for suffix in ('.bat', '.cmd', '.exe'):
1238 alt_target = target + suffix
1239 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1240 return alt_target
1241 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001242
1243
1244def freeze(obj):
1245 """Takes a generic object ``obj``, and returns an immutable version of it.
1246
1247 Supported types:
1248 * dict / OrderedDict -> FrozenDict
1249 * list -> tuple
1250 * set -> frozenset
1251 * any object with a working __hash__ implementation (assumes that hashable
1252 means immutable)
1253
1254 Will raise TypeError if you pass an object which is not hashable.
1255 """
1256 if isinstance(obj, dict):
1257 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.iteritems())
1258 elif isinstance(obj, (list, tuple)):
1259 return tuple(freeze(i) for i in obj)
1260 elif isinstance(obj, set):
1261 return frozenset(freeze(i) for i in obj)
1262 else:
1263 hash(obj)
1264 return obj
1265
1266
1267class FrozenDict(collections.Mapping):
1268 """An immutable OrderedDict.
1269
1270 Modified From: http://stackoverflow.com/a/2704866
1271 """
1272 def __init__(self, *args, **kwargs):
1273 self._d = collections.OrderedDict(*args, **kwargs)
1274
1275 # Calculate the hash immediately so that we know all the items are
1276 # hashable too.
1277 self._hash = reduce(operator.xor,
1278 (hash(i) for i in enumerate(self._d.iteritems())), 0)
1279
1280 def __eq__(self, other):
1281 if not isinstance(other, collections.Mapping):
1282 return NotImplemented
1283 if self is other:
1284 return True
1285 if len(self) != len(other):
1286 return False
1287 for k, v in self.iteritems():
1288 if k not in other or other[k] != v:
1289 return False
1290 return True
1291
1292 def __iter__(self):
1293 return iter(self._d)
1294
1295 def __len__(self):
1296 return len(self._d)
1297
1298 def __getitem__(self, key):
1299 return self._d[key]
1300
1301 def __hash__(self):
1302 return self._hash
1303
1304 def __repr__(self):
1305 return 'FrozenDict(%r)' % (self._d.items(),)