blob: 4d9e9860ff72be01e8f2069f3d30651190d84567 [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
Raul Tambreb946b232019-03-26 14:48:46 +00007from __future__ import print_function
8
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00009import codecs
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020010import collections
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +000011import contextlib
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000012import datetime
Raul Tambreb946b232019-03-26 14:48:46 +000013import functools
14import io
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000015import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020016import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000018import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000019import platform
msb@chromium.orgac915bb2009-11-13 17:03:01 +000020import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000021import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000022import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000023import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000024import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000025import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000026import time
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000027import subprocess2
28
Raul Tambreb946b232019-03-26 14:48:46 +000029if sys.version_info.major == 2:
30 from cStringIO import StringIO
Raul Tambre6693d092020-02-19 20:36:45 +000031 import collections as collections_abc
Edward Lemura8145022020-01-06 18:47:54 +000032 import Queue as queue
33 import urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000034else:
Raul Tambre6693d092020-02-19 20:36:45 +000035 from collections import abc as collections_abc
Raul Tambreb946b232019-03-26 14:48:46 +000036 from io import StringIO
Edward Lemura8145022020-01-06 18:47:54 +000037 import queue
38 import urllib.parse as urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000039
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000040
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000041RETRY_MAX = 3
42RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000043START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000044
45
borenet@google.com6a9b1682014-03-24 18:35:23 +000046_WARNINGS = []
47
48
szager@chromium.orgff113292014-03-25 06:02:08 +000049# These repos are known to cause OOM errors on 32-bit platforms, due the the
50# very large objects they contain. It is not safe to use threaded index-pack
51# when cloning/fetching them.
52THREADED_INDEX_PACK_BLACKLIST = [
53 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
54]
55
Raul Tambreb946b232019-03-26 14:48:46 +000056"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
57if sys.version_info.major == 2:
58 # We have to use exec to avoid a SyntaxError in Python 3.
59 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
60else:
61 def reraise(typ, value, tb=None):
62 if value is None:
63 value = typ()
64 if value.__traceback__ is not tb:
65 raise value.with_traceback(tb)
66 raise value
67
szager@chromium.orgff113292014-03-25 06:02:08 +000068
maruel@chromium.org66c83e62010-09-07 14:18:45 +000069class Error(Exception):
70 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000071 def __init__(self, msg, *args, **kwargs):
72 index = getattr(threading.currentThread(), 'index', 0)
73 if index:
74 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
75 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000076
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000077
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000078def Elapsed(until=None):
79 if until is None:
80 until = datetime.datetime.now()
81 return str(until - START).partition('.')[0]
82
83
borenet@google.com6a9b1682014-03-24 18:35:23 +000084def PrintWarnings():
85 """Prints any accumulated warnings."""
86 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000087 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000088 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000089 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000090
91
92def AddWarning(msg):
93 """Adds the given warning message to the list of accumulated warnings."""
94 _WARNINGS.append(msg)
95
96
msb@chromium.orgac915bb2009-11-13 17:03:01 +000097def SplitUrlRevision(url):
98 """Splits url and returns a two-tuple: url, rev"""
99 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000100 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000101 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000102 components = re.search(regex, url).groups()
103 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000104 components = url.rsplit('@', 1)
105 if re.match(r'^\w+\@', url) and '@' not in components[0]:
106 components = [url]
107
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000108 if len(components) == 1:
109 components += [None]
110 return tuple(components)
111
112
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000113def IsGitSha(revision):
114 """Returns true if the given string is a valid hex-encoded sha"""
115 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
116
117
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200118def IsFullGitSha(revision):
119 """Returns true if the given string is a valid hex-encoded full sha"""
120 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
121
122
floitsch@google.comeaab7842011-04-28 09:07:58 +0000123def IsDateRevision(revision):
124 """Returns true if the given revision is of the form "{ ... }"."""
125 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
126
127
128def MakeDateRevision(date):
129 """Returns a revision representing the latest revision before the given
130 date."""
131 return "{" + date + "}"
132
133
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000134def SyntaxErrorToError(filename, e):
135 """Raises a gclient_utils.Error exception with the human readable message"""
136 try:
137 # Try to construct a human readable error message
138 if filename:
139 error_message = 'There is a syntax error in %s\n' % filename
140 else:
141 error_message = 'There is a syntax error\n'
142 error_message += 'Line #%s, character %s: "%s"' % (
143 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
144 except:
145 # Something went wrong, re-raise the original exception
146 raise e
147 else:
148 raise Error(error_message)
149
150
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000151class PrintableObject(object):
152 def __str__(self):
153 output = ''
154 for i in dir(self):
155 if i.startswith('__'):
156 continue
157 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
158 return output
159
160
Edward Lemur419c92f2019-10-25 22:17:49 +0000161def FileRead(filename, mode='rbU'):
162 # Always decodes output to a Unicode string.
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000163 # On Python 3 newlines are converted to '\n' by default and 'U' is deprecated.
Edward Lemur419c92f2019-10-25 22:17:49 +0000164 if mode == 'rbU' and sys.version_info.major == 3:
165 mode = 'rb'
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000166 with open(filename, mode=mode) as f:
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000167 s = f.read()
Edward Lemur419c92f2019-10-25 22:17:49 +0000168 if isinstance(s, bytes):
169 return s.decode('utf-8', 'replace')
170 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000171
172
Edward Lemur1773f372020-02-22 00:27:14 +0000173def FileWrite(filename, content, mode='w', encoding='utf-8'):
174 with codecs.open(filename, mode=mode, encoding=encoding) as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000175 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000176
177
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000178@contextlib.contextmanager
179def temporary_directory(**kwargs):
180 tdir = tempfile.mkdtemp(**kwargs)
181 try:
182 yield tdir
183 finally:
184 if tdir:
185 rmtree(tdir)
186
187
Edward Lemur1773f372020-02-22 00:27:14 +0000188@contextlib.contextmanager
189def temporary_file():
190 """Creates a temporary file.
191
192 On Windows, a file must be closed before it can be opened again. This function
193 allows to write something like:
194
195 with gclient_utils.temporary_file() as tmp:
196 gclient_utils.FileWrite(tmp, foo)
197 useful_stuff(tmp)
198
199 Instead of something like:
200
201 with tempfile.NamedTemporaryFile(delete=False) as tmp:
202 tmp.write(foo)
203 tmp.close()
204 try:
205 useful_stuff(tmp)
206 finally:
207 os.remove(tmp.name)
208 """
209 handle, name = tempfile.mkstemp()
210 os.close(handle)
211 try:
212 yield name
213 finally:
214 os.remove(name)
215
216
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000217def safe_rename(old, new):
218 """Renames a file reliably.
219
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000220 Sometimes os.rename does not work because a dying git process keeps a handle
221 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000222 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000223 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000224 """
225 # roughly 10s
226 retries = 100
227 for i in range(retries):
228 try:
229 os.rename(old, new)
230 break
231 except OSError:
232 if i == (retries - 1):
233 # Give up.
234 raise
235 # retry
236 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
237 time.sleep(0.1)
238
239
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000240def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000241 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000242 os.remove(path)
243 else:
244 rmtree(path)
245
246
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000247def rmtree(path):
248 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000249
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000250 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000251
252 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700253 are read-only. We need to be able to force the files to be writable (i.e.,
254 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000255
256 Even with all this, Windows still sometimes fails to delete a file, citing
257 a permission error (maybe something to do with antivirus scans or disk
258 indexing). The best suggestion any of the user forums had was to wait a
259 bit and try again, so we do that too. It's hand-waving, but sometimes it
260 works. :/
261
262 On POSIX systems, things are a little bit simpler. The modes of the files
263 to be deleted doesn't matter, only the modes of the directories containing
264 them are significant. As the directory tree is traversed, each directory
265 has its mode set appropriately before descending into it. This should
266 result in the entire tree being removed, with the possible exception of
267 *path itself, because nothing attempts to change the mode of its parent.
268 Doing so would be hazardous, as it's not a directory slated for removal.
269 In the ordinary case, this is not a problem: for our purposes, the user
270 will never lack write permission on *path's parent.
271 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000272 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000273 return
274
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000275 if os.path.islink(path) or not os.path.isdir(path):
276 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000277
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000278 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000279 # Give up and use cmd.exe's rd command.
280 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000281 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000282 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
283 if exitcode == 0:
284 return
285 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000286 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000287 time.sleep(3)
288 raise Exception('Failed to remove path %s' % path)
289
290 # On POSIX systems, we need the x-bit set on the directory to access it,
291 # the r-bit to see its contents, and the w-bit to remove files from it.
292 # The actual modes of the files within the directory is irrelevant.
293 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000294
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000295 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000296 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000297
298 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000299 # If fullpath is a symbolic link that points to a directory, isdir will
300 # be True, but we don't want to descend into that as a directory, we just
301 # want to remove the link. Check islink and treat links as ordinary files
302 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000303 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000304 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000305 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000306 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000307 # Recurse.
308 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000309
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000310 remove(os.rmdir, path)
311
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000312
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000313def safe_makedirs(tree):
314 """Creates the directory in a safe manner.
315
316 Because multiple threads can create these directories concurently, trap the
317 exception and pass on.
318 """
319 count = 0
320 while not os.path.exists(tree):
321 count += 1
322 try:
323 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000324 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000325 # 17 POSIX, 183 Windows
326 if e.errno not in (17, 183):
327 raise
328 if count > 40:
329 # Give up.
330 raise
331
332
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000333def CommandToStr(args):
334 """Converts an arg list into a shell escaped string."""
335 return ' '.join(pipes.quote(arg) for arg in args)
336
337
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000338class Wrapper(object):
339 """Wraps an object, acting as a transparent proxy for all properties by
340 default.
341 """
342 def __init__(self, wrapped):
343 self._wrapped = wrapped
344
345 def __getattr__(self, name):
346 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000347
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000348
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000349class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000350 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000351 def __init__(self, wrapped, delay):
352 super(AutoFlush, self).__init__(wrapped)
353 if not hasattr(self, 'lock'):
354 self.lock = threading.Lock()
355 self.__last_flushed_at = time.time()
356 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000357
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000358 @property
359 def autoflush(self):
360 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000361
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000362 def write(self, out, *args, **kwargs):
363 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000364 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000365 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000366 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000367 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000368 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000369 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000370 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000371 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000372 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000373 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000374
375
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000376class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000377 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000378 threads with a NN> prefix.
379 """
380 def __init__(self, wrapped, include_zero=False):
381 super(Annotated, self).__init__(wrapped)
382 if not hasattr(self, 'lock'):
383 self.lock = threading.Lock()
384 self.__output_buffers = {}
385 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000386 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000387
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000388 @property
389 def annotated(self):
390 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000391
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000392 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000393 # Store as bytes to ensure Unicode characters get output correctly.
394 if not isinstance(out, bytes):
395 out = out.encode('utf-8')
396
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000397 index = getattr(threading.currentThread(), 'index', 0)
398 if not index and not self.__include_zero:
399 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000400 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000401
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000402 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000403 try:
404 # Use a dummy array to hold the string so the code can be lockless.
405 # Strings are immutable, requiring to keep a lock for the whole dictionary
406 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000407 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000408 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000409 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000410 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000411 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000412 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000413
414 # Continue lockless.
415 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000416 while True:
417 # TODO(agable): find both of these with a single pass.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000418 cr_loc = obj[0].find(b'\r')
419 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000420 if cr_loc == lf_loc == -1:
421 break
422 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000423 line, remaining = obj[0].split(b'\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000424 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000425 self._wrapped_write(b'%d>%s\n' % (index, line))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000426 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000427 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000428 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000429 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000430 obj[0] = remaining
431
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000432 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000433 """Flush buffered output."""
434 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000435 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000436 try:
437 # Detect threads no longer existing.
438 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000439 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000440 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000441 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000442 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000443 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000444 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000445 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000446 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000447
448 # Don't keep the lock while writting. Will append \n when it shouldn't.
449 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000450 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000451 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000452 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000453
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000454
455def MakeFileAutoFlush(fileobj, delay=10):
456 autoflush = getattr(fileobj, 'autoflush', None)
457 if autoflush:
458 autoflush.delay = delay
459 return fileobj
460 return AutoFlush(fileobj, delay)
461
462
463def MakeFileAnnotated(fileobj, include_zero=False):
464 if getattr(fileobj, 'annotated', None):
465 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000466 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000467
468
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000469GCLIENT_CHILDREN = []
470GCLIENT_CHILDREN_LOCK = threading.Lock()
471
472
473class GClientChildren(object):
474 @staticmethod
475 def add(popen_obj):
476 with GCLIENT_CHILDREN_LOCK:
477 GCLIENT_CHILDREN.append(popen_obj)
478
479 @staticmethod
480 def remove(popen_obj):
481 with GCLIENT_CHILDREN_LOCK:
482 GCLIENT_CHILDREN.remove(popen_obj)
483
484 @staticmethod
485 def _attemptToKillChildren():
486 global GCLIENT_CHILDREN
487 with GCLIENT_CHILDREN_LOCK:
488 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
489
490 for zombie in zombies:
491 try:
492 zombie.kill()
493 except OSError:
494 pass
495
496 with GCLIENT_CHILDREN_LOCK:
497 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
498
499 @staticmethod
500 def _areZombies():
501 with GCLIENT_CHILDREN_LOCK:
502 return bool(GCLIENT_CHILDREN)
503
504 @staticmethod
505 def KillAllRemainingChildren():
506 GClientChildren._attemptToKillChildren()
507
508 if GClientChildren._areZombies():
509 time.sleep(0.5)
510 GClientChildren._attemptToKillChildren()
511
512 with GCLIENT_CHILDREN_LOCK:
513 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000514 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000515 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000516 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000517
518
Edward Lemur24146be2019-08-01 21:44:52 +0000519def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
520 show_header=False, always_show_header=False, retry=False,
521 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000522 """Runs a command and calls back a filter function if needed.
523
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000524 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000525 print_stdout: If True, the command's stdout is forwarded to stdout.
526 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000527 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000528 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000529 show_header: Whether to display a header before the command output.
530 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000531 retry: If the process exits non-zero, sleep for a brief interval and try
532 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000533
534 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000535
536 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000537 """
Edward Lemur24146be2019-08-01 21:44:52 +0000538 def show_header_if_necessary(needs_header, attempt):
539 """Show the header at most once."""
540 if not needs_header[0]:
541 return
542
543 needs_header[0] = False
544 # Automatically generated header. We only prepend a newline if
545 # always_show_header is false, since it usually indicates there's an
546 # external progress display, and it's better not to clobber it in that case.
547 header = '' if always_show_header else '\n'
548 header += '________ running \'%s\' in \'%s\'' % (
549 ' '.join(args), kwargs.get('cwd', '.'))
550 if attempt:
551 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
552 header += '\n'
553
554 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000555 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
556 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000557 if filter_fn:
558 filter_fn(header)
559
560 def filter_line(command_output, line_start):
561 """Extract the last line from command output and filter it."""
562 if not filter_fn or line_start is None:
563 return
564 command_output.seek(line_start)
565 filter_fn(command_output.read().decode('utf-8'))
566
567 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
568 # byte inputs and sys.stdout.buffer must be used instead.
569 if print_stdout:
570 sys.stdout.flush()
571 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
572 else:
573 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000574
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000575 sleep_interval = RETRY_INITIAL_SLEEP
576 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000577 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000578 kid = subprocess2.Popen(
579 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
580 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000581
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000582 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000583
Edward Lemur24146be2019-08-01 21:44:52 +0000584 # Store the output of the command regardless of the value of print_stdout or
585 # filter_fn.
586 command_output = io.BytesIO()
587
588 # Passed as a list for "by ref" semantics.
589 needs_header = [show_header]
590 if always_show_header:
591 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000592
593 # Also, we need to forward stdout to prevent weird re-ordering of output.
594 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700595 # normally buffering is done for each line, but if the process requests
596 # input, no end-of-line character is output after the prompt and it would
597 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000598 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000599 line_start = None
600 while True:
601 in_byte = kid.stdout.read(1)
602 is_newline = in_byte in (b'\n', b'\r')
603 if not in_byte:
604 break
605
606 show_header_if_necessary(needs_header, attempt)
607
608 if is_newline:
609 filter_line(command_output, line_start)
610 line_start = None
611 elif line_start is None:
612 line_start = command_output.tell()
613
614 stdout_write(in_byte)
615 command_output.write(in_byte)
616
617 # Flush the rest of buffered output.
618 sys.stdout.flush()
619 if line_start is not None:
620 filter_line(command_output, line_start)
621
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000622 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000623 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000624
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000625 # Don't put this in a 'finally,' since the child may still run if we get
626 # an exception.
627 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000628
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000629 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000630 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000631 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000632
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000633 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000634 return command_output.getvalue()
635
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000636 if not retry:
637 break
Edward Lemur24146be2019-08-01 21:44:52 +0000638
Raul Tambreb946b232019-03-26 14:48:46 +0000639 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
640 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000641 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000642 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000643
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000644 raise subprocess2.CalledProcessError(
645 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000646
647
agable@chromium.org5a306a22014-02-24 22:13:59 +0000648class GitFilter(object):
649 """A filter_fn implementation for quieting down git output messages.
650
651 Allows a custom function to skip certain lines (predicate), and will throttle
652 the output of percentage completed lines to only output every X seconds.
653 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000654 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000655
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000656 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000657 """
658 Args:
659 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
660 XX% complete messages) to only be printed at least |time_throttle|
661 seconds apart.
662 predicate (f(line)): An optional function which is invoked for every line.
663 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000664 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000665 """
Edward Lemur24146be2019-08-01 21:44:52 +0000666 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000667 self.last_time = 0
668 self.time_throttle = time_throttle
669 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000670 self.out_fh = out_fh or sys.stdout
671 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000672
673 def __call__(self, line):
674 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000675 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000676 if esc > -1:
677 line = line[:esc]
678 if self.predicate and not self.predicate(line):
679 return
680 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000681 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000682 if match:
683 if match.group(1) != self.progress_prefix:
684 self.progress_prefix = match.group(1)
685 elif now - self.last_time < self.time_throttle:
686 return
687 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000688 if not self.first_line:
689 self.out_fh.write('[%s] ' % Elapsed())
690 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000691 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000692
693
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000694def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000695 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000696
rcui@google.com13595ff2011-10-13 01:25:07 +0000697 Returns nearest upper-level directory with the passed in file.
698 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000699 if not path:
700 path = os.getcwd()
701 path = os.path.realpath(path)
702 while True:
703 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000704 if os.path.exists(file_path):
705 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000706 (new_path, _) = os.path.split(path)
707 if new_path == path:
708 return None
709 path = new_path
710
711
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000712def GetMacWinOrLinux():
713 """Returns 'mac', 'win', or 'linux', matching the current platform."""
714 if sys.platform.startswith(('cygwin', 'win')):
715 return 'win'
716 elif sys.platform.startswith('linux'):
717 return 'linux'
718 elif sys.platform == 'darwin':
719 return 'mac'
720 raise Error('Unknown platform: ' + sys.platform)
721
722
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000723def GetGClientRootAndEntries(path=None):
724 """Returns the gclient root and the dict of entries."""
725 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000726 root = FindFileUpwards(config_file, path)
727 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000728 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000729 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000730 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000731 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000732 with open(config_path) as config:
733 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000734 config_dir = os.path.dirname(config_path)
735 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000736
737
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000738def lockedmethod(method):
739 """Method decorator that holds self.lock for the duration of the call."""
740 def inner(self, *args, **kwargs):
741 try:
742 try:
743 self.lock.acquire()
744 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000745 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000746 raise
747 return method(self, *args, **kwargs)
748 finally:
749 self.lock.release()
750 return inner
751
752
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000753class WorkItem(object):
754 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000755 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
756 # As a workaround, use a single lock. Yep you read it right. Single lock for
757 # all the 100 objects.
758 lock = threading.Lock()
759
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000760 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000761 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000762 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000763 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000764 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700765 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000766
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000767 def run(self, work_queue):
768 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000769 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000770 pass
771
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000772 @property
773 def name(self):
774 return self._name
775
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000776
777class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000778 """Runs a set of WorkItem that have interdependencies and were WorkItem are
779 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000780
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200781 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000782 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000783
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000784 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000785 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000786 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000787 """jobs specifies the number of concurrent tasks to allow. progress is a
788 Progress instance."""
789 # Set when a thread is done or a new item is enqueued.
790 self.ready_cond = threading.Condition()
791 # Maximum number of concurrent tasks.
792 self.jobs = jobs
793 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000794 self.queued = []
795 # List of strings representing each Dependency.name that was run.
796 self.ran = []
797 # List of items currently running.
798 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000799 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000800 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000801 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000802 self.progress = progress
803 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000804 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000805
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000806 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000807 self.verbose = verbose
808 self.last_join = None
809 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000810
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000811 def enqueue(self, d):
812 """Enqueue one Dependency to be executed later once its requirements are
813 satisfied.
814 """
815 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000816 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000817 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000818 self.queued.append(d)
819 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000820 if self.jobs == 1:
821 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000822 logging.debug('enqueued(%s)' % d.name)
823 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000824 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000825 self.progress.update(0)
826 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000827 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000828 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000829
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000830 def out_cb(self, _):
831 self.last_subproc_output = datetime.datetime.now()
832 return True
833
834 @staticmethod
835 def format_task_output(task, comment=''):
836 if comment:
837 comment = ' (%s)' % comment
838 if task.start and task.finish:
839 elapsed = ' (Elapsed: %s)' % (
840 str(task.finish - task.start).partition('.')[0])
841 else:
842 elapsed = ''
843 return """
844%s%s%s
845----------------------------------------
846%s
847----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000848 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000849
hinoka885e5b12016-06-08 14:40:09 -0700850 def _is_conflict(self, job):
851 """Checks to see if a job will conflict with another running job."""
852 for running_job in self.running:
853 for used_resource in running_job.item.resources:
854 logging.debug('Checking resource %s' % used_resource)
855 if used_resource in job.resources:
856 return True
857 return False
858
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000859 def flush(self, *args, **kwargs):
860 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000861 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000862 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000863 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000864 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000865 while True:
866 # Check for task to run first, then wait.
867 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000868 if not self.exceptions.empty():
869 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000870 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000871 self._flush_terminated_threads()
872 if (not self.queued and not self.running or
873 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000874 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000875 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000876
877 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000878 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000879 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000880 if (self.ignore_requirements or
881 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700882 if not self._is_conflict(self.queued[i]):
883 # Start one work item: all its requirements are satisfied.
884 self._run_one_task(self.queued.pop(i), args, kwargs)
885 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000886 else:
887 # Couldn't find an item that could run. Break out the outher loop.
888 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000889
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000890 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000891 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000892 break
893 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000894 try:
895 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000896 # If we haven't printed to terminal for a while, but we have received
897 # spew from a suprocess, let the user know we're still progressing.
898 now = datetime.datetime.now()
899 if (now - self.last_join > datetime.timedelta(seconds=60) and
900 self.last_subproc_output > self.last_join):
901 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000902 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000903 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000904 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000905 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000906 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000907 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000908 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000909 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000910 except KeyboardInterrupt:
911 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000912 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000913 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000914 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
915 self.ran), len(self.running)),
916 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000917 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000918 print(
919 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
920 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000921 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000922 print(
923 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000924 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000925 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000926 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000927 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000928
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000929 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000930 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000931 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000932 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000933 # To get back the stack location correctly, the raise a, b, c form must be
934 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000935 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000936 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
937 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000938 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000939 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000940
maruel@chromium.org3742c842010-09-09 19:27:14 +0000941 def _flush_terminated_threads(self):
942 """Flush threads that have terminated."""
943 running = self.running
944 self.running = []
945 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000946 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000947 self.running.append(t)
948 else:
949 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000950 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000951 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000952 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000953 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000954 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000955 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000956 if t.item.name in self.ran:
957 raise Error(
958 'gclient is confused, "%s" is already in "%s"' % (
959 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000960 if not t.item.name in self.ran:
961 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000962
963 def _run_one_task(self, task_item, args, kwargs):
964 if self.jobs > 1:
965 # Start the thread.
966 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000967 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000968 self.running.append(new_thread)
969 new_thread.start()
970 else:
971 # Run the 'thread' inside the main thread. Don't try to catch any
972 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000973 try:
974 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000975 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000976 task_item.run(*args, **kwargs)
977 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000978 print(
979 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000980 self.ran.append(task_item.name)
981 if self.verbose:
982 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000983 print('')
984 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000985 if self.progress:
986 self.progress.update(1, ', '.join(t.item.name for t in self.running))
987 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000988 print(
989 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000990 raise
991 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000992 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000993 raise
994
maruel@chromium.org3742c842010-09-09 19:27:14 +0000995
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000996 class _Worker(threading.Thread):
997 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000998 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000999 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001000 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001001 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001002 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001003 self.args = args
1004 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001005 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001006
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001007 def run(self):
1008 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001009 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001010 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001011 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001012 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001013 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001014 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001015 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001016 print(
1017 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001018 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001019 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001020 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001021 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001022 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001023 except Exception:
1024 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001025 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001026 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001027 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001028 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001029 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001030 work_queue.ready_cond.acquire()
1031 try:
1032 work_queue.ready_cond.notifyAll()
1033 finally:
1034 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001035
1036
agable92bec4f2016-08-24 09:27:27 -07001037def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001038 """Returns the most plausible editor to use.
1039
1040 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001041 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001042 - core.editor git configuration variable (if supplied by git-cl)
1043 - VISUAL environment variable
1044 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001045 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001046
1047 In the case of git-cl, this matches git's behaviour, except that it does not
1048 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001049 """
agable92bec4f2016-08-24 09:27:27 -07001050 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001051 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001052 editor = os.environ.get('VISUAL')
1053 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001054 editor = os.environ.get('EDITOR')
1055 if not editor:
1056 if sys.platform.startswith('win'):
1057 editor = 'notepad'
1058 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001059 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001060 return editor
1061
1062
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001063def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001064 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001065 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001066 # Make sure CRLF is handled properly by requiring none.
1067 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001068 print(
1069 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001070 fileobj = os.fdopen(file_handle, 'w')
1071 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001072 content = re.sub('\r?\n', '\n', content)
1073 # Some editors complain when the file doesn't end in \n.
1074 if not content.endswith('\n'):
1075 content += '\n'
1076 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001077 fileobj.close()
1078
1079 try:
agable92bec4f2016-08-24 09:27:27 -07001080 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001081 if not editor:
1082 return None
1083 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001084 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1085 # Msysgit requires the usage of 'env' to be present.
1086 cmd = 'env ' + cmd
1087 try:
1088 # shell=True to allow the shell to handle all forms of quotes in
1089 # $EDITOR.
1090 subprocess2.check_call(cmd, shell=True)
1091 except subprocess2.CalledProcessError:
1092 return None
1093 return FileRead(filename)
1094 finally:
1095 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001096
1097
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001098def UpgradeToHttps(url):
1099 """Upgrades random urls to https://.
1100
1101 Do not touch unknown urls like ssh:// or git://.
1102 Do not touch http:// urls with a port number,
1103 Fixes invalid GAE url.
1104 """
1105 if not url:
1106 return url
1107 if not re.match(r'[a-z\-]+\://.*', url):
1108 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1109 # relative url and will use http:///foo. Note that it defaults to http://
1110 # for compatibility with naked url like "localhost:8080".
1111 url = 'http://%s' % url
1112 parsed = list(urlparse.urlparse(url))
1113 # Do not automatically upgrade http to https if a port number is provided.
1114 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1115 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001116 return urlparse.urlunparse(parsed)
1117
1118
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001119def ParseCodereviewSettingsContent(content):
1120 """Process a codereview.settings file properly."""
1121 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1122 try:
1123 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1124 except ValueError:
1125 raise Error(
1126 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001127 def fix_url(key):
1128 if keyvals.get(key):
1129 keyvals[key] = UpgradeToHttps(keyvals[key])
1130 fix_url('CODE_REVIEW_SERVER')
1131 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001132 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001133
1134
1135def NumLocalCpus():
1136 """Returns the number of processors.
1137
dnj@chromium.org530523b2015-01-07 19:54:57 +00001138 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1139 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1140 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001141 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001142 # Surround the entire thing in try/except; no failure here should stop gclient
1143 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001144 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001145 # Use multiprocessing to get CPU count. This may raise
1146 # NotImplementedError.
1147 try:
1148 import multiprocessing
1149 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001150 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001151 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001152 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001153 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1154 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1155
1156 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1157 if 'NUMBER_OF_PROCESSORS' in os.environ:
1158 return int(os.environ['NUMBER_OF_PROCESSORS'])
1159 except Exception as e:
1160 logging.exception("Exception raised while probing CPU count: %s", e)
1161
1162 logging.debug('Failed to get CPU count. Defaulting to 1.')
1163 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001164
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001165
szager@chromium.orgfc616382014-03-18 20:32:04 +00001166def DefaultDeltaBaseCacheLimit():
1167 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1168
1169 The primary constraint is the address space of virtual memory. The cache
1170 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1171 parameter is set too high.
1172 """
1173 if platform.architecture()[0].startswith('64'):
1174 return '2g'
1175 else:
1176 return '512m'
1177
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001178
szager@chromium.orgff113292014-03-25 06:02:08 +00001179def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001180 """Return reasonable default values for configuring git-index-pack.
1181
1182 Experiments suggest that higher values for pack.threads don't improve
1183 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001184 cache_limit = DefaultDeltaBaseCacheLimit()
1185 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1186 if url in THREADED_INDEX_PACK_BLACKLIST:
1187 result.extend(['-c', 'pack.threads=1'])
1188 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001189
1190
1191def FindExecutable(executable):
1192 """This mimics the "which" utility."""
1193 path_folders = os.environ.get('PATH').split(os.pathsep)
1194
1195 for path_folder in path_folders:
1196 target = os.path.join(path_folder, executable)
1197 # Just incase we have some ~/blah paths.
1198 target = os.path.abspath(os.path.expanduser(target))
1199 if os.path.isfile(target) and os.access(target, os.X_OK):
1200 return target
1201 if sys.platform.startswith('win'):
1202 for suffix in ('.bat', '.cmd', '.exe'):
1203 alt_target = target + suffix
1204 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1205 return alt_target
1206 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001207
1208
1209def freeze(obj):
1210 """Takes a generic object ``obj``, and returns an immutable version of it.
1211
1212 Supported types:
1213 * dict / OrderedDict -> FrozenDict
1214 * list -> tuple
1215 * set -> frozenset
1216 * any object with a working __hash__ implementation (assumes that hashable
1217 means immutable)
1218
1219 Will raise TypeError if you pass an object which is not hashable.
1220 """
Raul Tambre6693d092020-02-19 20:36:45 +00001221 if isinstance(obj, collections_abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001222 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001223 elif isinstance(obj, (list, tuple)):
1224 return tuple(freeze(i) for i in obj)
1225 elif isinstance(obj, set):
1226 return frozenset(freeze(i) for i in obj)
1227 else:
1228 hash(obj)
1229 return obj
1230
1231
Raul Tambre6693d092020-02-19 20:36:45 +00001232class FrozenDict(collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001233 """An immutable OrderedDict.
1234
1235 Modified From: http://stackoverflow.com/a/2704866
1236 """
1237 def __init__(self, *args, **kwargs):
1238 self._d = collections.OrderedDict(*args, **kwargs)
1239
1240 # Calculate the hash immediately so that we know all the items are
1241 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001242 self._hash = functools.reduce(
1243 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001244
1245 def __eq__(self, other):
Raul Tambre6693d092020-02-19 20:36:45 +00001246 if not isinstance(other, collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001247 return NotImplemented
1248 if self is other:
1249 return True
1250 if len(self) != len(other):
1251 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001252 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001253 if k not in other or other[k] != v:
1254 return False
1255 return True
1256
1257 def __iter__(self):
1258 return iter(self._d)
1259
1260 def __len__(self):
1261 return len(self._d)
1262
1263 def __getitem__(self, key):
1264 return self._d[key]
1265
1266 def __hash__(self):
1267 return self._hash
1268
1269 def __repr__(self):
1270 return 'FrozenDict(%r)' % (self._d.items(),)