blob: e60c0693978e6e4dd585ec6724ca9ffa9cfa744c [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.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000616 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000617 "file you want to use" % (filename, path))
618 return path
619 scope = {}
620 try:
621 exec(FileRead(entries_filename), scope)
622 except SyntaxError, e:
623 SyntaxErrorToError(filename, e)
624 all_directories = scope['entries'].keys()
625 path_to_check = real_from_dir[len(path)+1:]
626 while path_to_check:
627 if path_to_check in all_directories:
628 return path
629 path_to_check = os.path.dirname(path_to_check)
630 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000631
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000632 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000633 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000634
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000635
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000636def PathDifference(root, subpath):
637 """Returns the difference subpath minus root."""
638 root = os.path.realpath(root)
639 subpath = os.path.realpath(subpath)
640 if not subpath.startswith(root):
641 return None
642 # If the root does not have a trailing \ or /, we add it so the returned
643 # path starts immediately after the seperator regardless of whether it is
644 # provided.
645 root = os.path.join(root, '')
646 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000647
648
649def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000650 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000651
rcui@google.com13595ff2011-10-13 01:25:07 +0000652 Returns nearest upper-level directory with the passed in file.
653 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000654 if not path:
655 path = os.getcwd()
656 path = os.path.realpath(path)
657 while True:
658 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000659 if os.path.exists(file_path):
660 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000661 (new_path, _) = os.path.split(path)
662 if new_path == path:
663 return None
664 path = new_path
665
666
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000667def GetMacWinOrLinux():
668 """Returns 'mac', 'win', or 'linux', matching the current platform."""
669 if sys.platform.startswith(('cygwin', 'win')):
670 return 'win'
671 elif sys.platform.startswith('linux'):
672 return 'linux'
673 elif sys.platform == 'darwin':
674 return 'mac'
675 raise Error('Unknown platform: ' + sys.platform)
676
677
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000678def GetPrimarySolutionPath():
679 """Returns the full path to the primary solution. (gclient_root + src)"""
zturner@chromium.org0db9a142014-08-13 23:15:25 +0000680
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000681 gclient_root = FindGclientRoot(os.getcwd())
682 if not gclient_root:
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000683 # Some projects might not use .gclient. Try to see whether we're in a git
684 # checkout.
685 top_dir = [os.getcwd()]
686 def filter_fn(line):
hanpfei13f9c372016-08-08 22:05:56 -0700687 repo_root_path = os.path.normpath(line.rstrip('\n'))
688 if os.path.exists(repo_root_path):
689 top_dir[0] = repo_root_path
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000690 try:
691 CheckCallAndFilter(["git", "rev-parse", "--show-toplevel"],
692 print_stdout=False, filter_fn=filter_fn)
693 except Exception:
694 pass
695 top_dir = top_dir[0]
696 if os.path.exists(os.path.join(top_dir, 'buildtools')):
jiangj@opera.comd6d15b82015-04-20 06:43:48 +0000697 return top_dir
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000698 return None
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000699
700 # Some projects' top directory is not named 'src'.
701 source_dir_name = GetGClientPrimarySolutionName(gclient_root) or 'src'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000702 return os.path.join(gclient_root, source_dir_name)
703
704
705def GetBuildtoolsPath():
706 """Returns the full path to the buildtools directory.
707 This is based on the root of the checkout containing the current directory."""
708
709 # Overriding the build tools path by environment is highly unsupported and may
710 # break without warning. Do not rely on this for anything important.
711 override = os.environ.get('CHROMIUM_BUILDTOOLS_PATH')
712 if override is not None:
713 return override
714
715 primary_solution = GetPrimarySolutionPath()
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000716 if not primary_solution:
717 return None
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000718 buildtools_path = os.path.join(primary_solution, 'buildtools')
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000719 if not os.path.exists(buildtools_path):
720 # Buildtools may be in the gclient root.
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000721 gclient_root = FindGclientRoot(os.getcwd())
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000722 buildtools_path = os.path.join(gclient_root, 'buildtools')
723 return buildtools_path
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000724
725
726def GetBuildtoolsPlatformBinaryPath():
727 """Returns the full path to the binary directory for the current platform."""
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000728 buildtools_path = GetBuildtoolsPath()
729 if not buildtools_path:
730 return None
731
732 if sys.platform.startswith(('cygwin', 'win')):
733 subdir = 'win'
734 elif sys.platform == 'darwin':
735 subdir = 'mac'
736 elif sys.platform.startswith('linux'):
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000737 subdir = 'linux64'
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000738 else:
739 raise Error('Unknown platform: ' + sys.platform)
740 return os.path.join(buildtools_path, subdir)
741
742
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000743def GetExeSuffix():
744 """Returns '' or '.exe' depending on how executables work on this platform."""
745 if sys.platform.startswith(('cygwin', 'win')):
746 return '.exe'
747 return ''
748
749
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000750def GetGClientPrimarySolutionName(gclient_root_dir_path):
751 """Returns the name of the primary solution in the .gclient file specified."""
752 gclient_config_file = os.path.join(gclient_root_dir_path, '.gclient')
753 env = {}
754 execfile(gclient_config_file, env)
755 solutions = env.get('solutions', [])
756 if solutions:
757 return solutions[0].get('name')
758 return None
759
760
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000761def GetGClientRootAndEntries(path=None):
762 """Returns the gclient root and the dict of entries."""
763 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000764 root = FindFileUpwards(config_file, path)
765 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000766 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000767 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000768 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000769 env = {}
770 execfile(config_path, env)
771 config_dir = os.path.dirname(config_path)
772 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000773
774
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000775def lockedmethod(method):
776 """Method decorator that holds self.lock for the duration of the call."""
777 def inner(self, *args, **kwargs):
778 try:
779 try:
780 self.lock.acquire()
781 except KeyboardInterrupt:
782 print >> sys.stderr, 'Was deadlocked'
783 raise
784 return method(self, *args, **kwargs)
785 finally:
786 self.lock.release()
787 return inner
788
789
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000790class WorkItem(object):
791 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000792 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
793 # As a workaround, use a single lock. Yep you read it right. Single lock for
794 # all the 100 objects.
795 lock = threading.Lock()
796
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000797 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000798 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000799 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000800 self.outbuf = cStringIO.StringIO()
801 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700802 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000803
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000804 def run(self, work_queue):
805 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000806 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000807 pass
808
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000809 @property
810 def name(self):
811 return self._name
812
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000813
814class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000815 """Runs a set of WorkItem that have interdependencies and were WorkItem are
816 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000817
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200818 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000819 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000820
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000821 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000822 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000823 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000824 """jobs specifies the number of concurrent tasks to allow. progress is a
825 Progress instance."""
826 # Set when a thread is done or a new item is enqueued.
827 self.ready_cond = threading.Condition()
828 # Maximum number of concurrent tasks.
829 self.jobs = jobs
830 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000831 self.queued = []
832 # List of strings representing each Dependency.name that was run.
833 self.ran = []
834 # List of items currently running.
835 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000836 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000837 self.exceptions = Queue.Queue()
838 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000839 self.progress = progress
840 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000841 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000842
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000843 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000844 self.verbose = verbose
845 self.last_join = None
846 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000847
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000848 def enqueue(self, d):
849 """Enqueue one Dependency to be executed later once its requirements are
850 satisfied.
851 """
852 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000853 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000854 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000855 self.queued.append(d)
856 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000857 if self.jobs == 1:
858 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000859 logging.debug('enqueued(%s)' % d.name)
860 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000861 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000862 self.progress.update(0)
863 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000864 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000865 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000866
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000867 def out_cb(self, _):
868 self.last_subproc_output = datetime.datetime.now()
869 return True
870
871 @staticmethod
872 def format_task_output(task, comment=''):
873 if comment:
874 comment = ' (%s)' % comment
875 if task.start and task.finish:
876 elapsed = ' (Elapsed: %s)' % (
877 str(task.finish - task.start).partition('.')[0])
878 else:
879 elapsed = ''
880 return """
881%s%s%s
882----------------------------------------
883%s
884----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000885 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000886
hinoka885e5b12016-06-08 14:40:09 -0700887 def _is_conflict(self, job):
888 """Checks to see if a job will conflict with another running job."""
889 for running_job in self.running:
890 for used_resource in running_job.item.resources:
891 logging.debug('Checking resource %s' % used_resource)
892 if used_resource in job.resources:
893 return True
894 return False
895
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000896 def flush(self, *args, **kwargs):
897 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000898 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000899 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000900 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000901 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000902 while True:
903 # Check for task to run first, then wait.
904 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000905 if not self.exceptions.empty():
906 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000907 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000908 self._flush_terminated_threads()
909 if (not self.queued and not self.running or
910 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000911 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000912 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000913
914 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000915 for i in xrange(len(self.queued)):
916 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000917 if (self.ignore_requirements or
918 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700919 if not self._is_conflict(self.queued[i]):
920 # Start one work item: all its requirements are satisfied.
921 self._run_one_task(self.queued.pop(i), args, kwargs)
922 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000923 else:
924 # Couldn't find an item that could run. Break out the outher loop.
925 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000926
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000927 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000928 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000929 break
930 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000931 try:
932 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000933 # If we haven't printed to terminal for a while, but we have received
934 # spew from a suprocess, let the user know we're still progressing.
935 now = datetime.datetime.now()
936 if (now - self.last_join > datetime.timedelta(seconds=60) and
937 self.last_subproc_output > self.last_join):
938 if self.progress:
939 print >> sys.stdout, ''
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000940 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000941 elapsed = Elapsed()
942 print >> sys.stdout, '[%s] Still working on:' % elapsed
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000943 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000944 for task in self.running:
945 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000946 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000947 except KeyboardInterrupt:
948 # Help debugging by printing some information:
949 print >> sys.stderr, (
950 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
951 'Running: %d') % (
952 self.jobs,
953 len(self.queued),
954 ', '.join(self.ran),
955 len(self.running)))
956 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000957 print >> sys.stderr, '%s (not started): %s' % (
958 i.name, ', '.join(i.requirements))
959 for i in self.running:
960 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000961 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000962 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000963 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000964 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000965
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000966 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000967 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000968 if self.progress:
969 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000970 # To get back the stack location correctly, the raise a, b, c form must be
971 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000972 e, task = self.exceptions.get()
973 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000974 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000975 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000976 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000977
maruel@chromium.org3742c842010-09-09 19:27:14 +0000978 def _flush_terminated_threads(self):
979 """Flush threads that have terminated."""
980 running = self.running
981 self.running = []
982 for t in running:
983 if t.isAlive():
984 self.running.append(t)
985 else:
986 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000987 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000988 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000989 if self.verbose:
990 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000991 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000992 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000993 if t.item.name in self.ran:
994 raise Error(
995 'gclient is confused, "%s" is already in "%s"' % (
996 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000997 if not t.item.name in self.ran:
998 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000999
1000 def _run_one_task(self, task_item, args, kwargs):
1001 if self.jobs > 1:
1002 # Start the thread.
1003 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001004 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001005 self.running.append(new_thread)
1006 new_thread.start()
1007 else:
1008 # Run the 'thread' inside the main thread. Don't try to catch any
1009 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001010 try:
1011 task_item.start = datetime.datetime.now()
1012 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
1013 task_item.run(*args, **kwargs)
1014 task_item.finish = datetime.datetime.now()
1015 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
1016 self.ran.append(task_item.name)
1017 if self.verbose:
1018 if self.progress:
1019 print >> sys.stdout, ''
1020 print >> sys.stdout, self.format_task_output(task_item)
1021 if self.progress:
1022 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1023 except KeyboardInterrupt:
1024 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
1025 raise
1026 except Exception:
1027 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
1028 raise
1029
maruel@chromium.org3742c842010-09-09 19:27:14 +00001030
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001031 class _Worker(threading.Thread):
1032 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001033 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001034 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001035 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001036 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001037 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001038 self.args = args
1039 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001040 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001041
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001042 def run(self):
1043 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001044 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001045 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001046 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001047 self.item.start = datetime.datetime.now()
1048 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001049 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001050 self.item.finish = datetime.datetime.now()
1051 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001052 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001053 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001054 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001055 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001056 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001057 except Exception:
1058 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001059 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001060 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001061 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001062 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001063 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001064 work_queue.ready_cond.acquire()
1065 try:
1066 work_queue.ready_cond.notifyAll()
1067 finally:
1068 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001069
1070
agable92bec4f2016-08-24 09:27:27 -07001071def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001072 """Returns the most plausible editor to use.
1073
1074 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001075 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001076 - core.editor git configuration variable (if supplied by git-cl)
1077 - VISUAL environment variable
1078 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001079 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001080
1081 In the case of git-cl, this matches git's behaviour, except that it does not
1082 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001083 """
agable92bec4f2016-08-24 09:27:27 -07001084 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001085 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001086 editor = os.environ.get('VISUAL')
1087 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001088 editor = os.environ.get('EDITOR')
1089 if not editor:
1090 if sys.platform.startswith('win'):
1091 editor = 'notepad'
1092 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001093 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001094 return editor
1095
1096
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001097def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001098 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001099 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001100 # Make sure CRLF is handled properly by requiring none.
1101 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +00001102 print >> sys.stderr, (
1103 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001104 fileobj = os.fdopen(file_handle, 'w')
1105 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001106 content = re.sub('\r?\n', '\n', content)
1107 # Some editors complain when the file doesn't end in \n.
1108 if not content.endswith('\n'):
1109 content += '\n'
1110 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001111 fileobj.close()
1112
1113 try:
agable92bec4f2016-08-24 09:27:27 -07001114 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001115 if not editor:
1116 return None
1117 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001118 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1119 # Msysgit requires the usage of 'env' to be present.
1120 cmd = 'env ' + cmd
1121 try:
1122 # shell=True to allow the shell to handle all forms of quotes in
1123 # $EDITOR.
1124 subprocess2.check_call(cmd, shell=True)
1125 except subprocess2.CalledProcessError:
1126 return None
1127 return FileRead(filename)
1128 finally:
1129 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001130
1131
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001132def UpgradeToHttps(url):
1133 """Upgrades random urls to https://.
1134
1135 Do not touch unknown urls like ssh:// or git://.
1136 Do not touch http:// urls with a port number,
1137 Fixes invalid GAE url.
1138 """
1139 if not url:
1140 return url
1141 if not re.match(r'[a-z\-]+\://.*', url):
1142 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1143 # relative url and will use http:///foo. Note that it defaults to http://
1144 # for compatibility with naked url like "localhost:8080".
1145 url = 'http://%s' % url
1146 parsed = list(urlparse.urlparse(url))
1147 # Do not automatically upgrade http to https if a port number is provided.
1148 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1149 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001150 return urlparse.urlunparse(parsed)
1151
1152
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001153def ParseCodereviewSettingsContent(content):
1154 """Process a codereview.settings file properly."""
1155 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1156 try:
1157 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1158 except ValueError:
1159 raise Error(
1160 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001161 def fix_url(key):
1162 if keyvals.get(key):
1163 keyvals[key] = UpgradeToHttps(keyvals[key])
1164 fix_url('CODE_REVIEW_SERVER')
1165 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001166 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001167
1168
1169def NumLocalCpus():
1170 """Returns the number of processors.
1171
dnj@chromium.org530523b2015-01-07 19:54:57 +00001172 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1173 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1174 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001175 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001176 # Surround the entire thing in try/except; no failure here should stop gclient
1177 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001178 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001179 # Use multiprocessing to get CPU count. This may raise
1180 # NotImplementedError.
1181 try:
1182 import multiprocessing
1183 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001184 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001185 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001186 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001187 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1188 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1189
1190 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1191 if 'NUMBER_OF_PROCESSORS' in os.environ:
1192 return int(os.environ['NUMBER_OF_PROCESSORS'])
1193 except Exception as e:
1194 logging.exception("Exception raised while probing CPU count: %s", e)
1195
1196 logging.debug('Failed to get CPU count. Defaulting to 1.')
1197 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001198
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001199
szager@chromium.orgfc616382014-03-18 20:32:04 +00001200def DefaultDeltaBaseCacheLimit():
1201 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1202
1203 The primary constraint is the address space of virtual memory. The cache
1204 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1205 parameter is set too high.
1206 """
1207 if platform.architecture()[0].startswith('64'):
1208 return '2g'
1209 else:
1210 return '512m'
1211
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001212
szager@chromium.orgff113292014-03-25 06:02:08 +00001213def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001214 """Return reasonable default values for configuring git-index-pack.
1215
1216 Experiments suggest that higher values for pack.threads don't improve
1217 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001218 cache_limit = DefaultDeltaBaseCacheLimit()
1219 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1220 if url in THREADED_INDEX_PACK_BLACKLIST:
1221 result.extend(['-c', 'pack.threads=1'])
1222 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001223
1224
1225def FindExecutable(executable):
1226 """This mimics the "which" utility."""
1227 path_folders = os.environ.get('PATH').split(os.pathsep)
1228
1229 for path_folder in path_folders:
1230 target = os.path.join(path_folder, executable)
1231 # Just incase we have some ~/blah paths.
1232 target = os.path.abspath(os.path.expanduser(target))
1233 if os.path.isfile(target) and os.access(target, os.X_OK):
1234 return target
1235 if sys.platform.startswith('win'):
1236 for suffix in ('.bat', '.cmd', '.exe'):
1237 alt_target = target + suffix
1238 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1239 return alt_target
1240 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001241
1242
1243def freeze(obj):
1244 """Takes a generic object ``obj``, and returns an immutable version of it.
1245
1246 Supported types:
1247 * dict / OrderedDict -> FrozenDict
1248 * list -> tuple
1249 * set -> frozenset
1250 * any object with a working __hash__ implementation (assumes that hashable
1251 means immutable)
1252
1253 Will raise TypeError if you pass an object which is not hashable.
1254 """
1255 if isinstance(obj, dict):
1256 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.iteritems())
1257 elif isinstance(obj, (list, tuple)):
1258 return tuple(freeze(i) for i in obj)
1259 elif isinstance(obj, set):
1260 return frozenset(freeze(i) for i in obj)
1261 else:
1262 hash(obj)
1263 return obj
1264
1265
1266class FrozenDict(collections.Mapping):
1267 """An immutable OrderedDict.
1268
1269 Modified From: http://stackoverflow.com/a/2704866
1270 """
1271 def __init__(self, *args, **kwargs):
1272 self._d = collections.OrderedDict(*args, **kwargs)
1273
1274 # Calculate the hash immediately so that we know all the items are
1275 # hashable too.
1276 self._hash = reduce(operator.xor,
1277 (hash(i) for i in enumerate(self._d.iteritems())), 0)
1278
1279 def __eq__(self, other):
1280 if not isinstance(other, collections.Mapping):
1281 return NotImplemented
1282 if self is other:
1283 return True
1284 if len(self) != len(other):
1285 return False
1286 for k, v in self.iteritems():
1287 if k not in other or other[k] != v:
1288 return False
1289 return True
1290
1291 def __iter__(self):
1292 return iter(self._d)
1293
1294 def __len__(self):
1295 return len(self._d)
1296
1297 def __getitem__(self, key):
1298 return self._d[key]
1299
1300 def __hash__(self):
1301 return self._hash
1302
1303 def __repr__(self):
1304 return 'FrozenDict(%r)' % (self._d.items(),)