utils.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2016 Google Inc.
  4. #
  5. # Use of this source code is governed by a BSD-style license that can be
  6. # found in the LICENSE file.
  7. import datetime
  8. import errno
  9. import os
  10. import shutil
  11. import sys
  12. import subprocess
  13. import tempfile
  14. import time
  15. import uuid
  16. GCLIENT = 'gclient.bat' if sys.platform == 'win32' else 'gclient'
  17. WHICH = 'where' if sys.platform == 'win32' else 'which'
  18. GIT = subprocess.check_output([WHICH, 'git']).splitlines()[0]
  19. class print_timings(object):
  20. def __init__(self):
  21. self._start = None
  22. def __enter__(self):
  23. self._start = datetime.datetime.utcnow()
  24. print 'Task started at %s GMT' % str(self._start)
  25. def __exit__(self, t, v, tb):
  26. finish = datetime.datetime.utcnow()
  27. duration = (finish-self._start).total_seconds()
  28. print 'Task finished at %s GMT (%f seconds)' % (str(finish), duration)
  29. class tmp_dir(object):
  30. """Helper class used for creating a temporary directory and working in it."""
  31. def __init__(self):
  32. self._orig_dir = None
  33. self._tmp_dir = None
  34. def __enter__(self):
  35. self._orig_dir = os.getcwd()
  36. self._tmp_dir = tempfile.mkdtemp()
  37. os.chdir(self._tmp_dir)
  38. return self
  39. def __exit__(self, t, v, tb):
  40. os.chdir(self._orig_dir)
  41. RemoveDirectory(self._tmp_dir)
  42. @property
  43. def name(self):
  44. return self._tmp_dir
  45. class chdir(object):
  46. """Helper class used for changing into and out of a directory."""
  47. def __init__(self, d):
  48. self._dir = d
  49. self._orig_dir = None
  50. def __enter__(self):
  51. self._orig_dir = os.getcwd()
  52. os.chdir(self._dir)
  53. return self
  54. def __exit__(self, t, v, tb):
  55. os.chdir(self._orig_dir)
  56. def git_clone(repo_url, dest_dir):
  57. """Clone the given repo into the given destination directory."""
  58. subprocess.check_call([GIT, 'clone', repo_url, dest_dir])
  59. class git_branch(object):
  60. """Check out a temporary git branch.
  61. On exit, deletes the branch and attempts to restore the original state.
  62. """
  63. def __init__(self):
  64. self._branch = None
  65. self._orig_branch = None
  66. self._stashed = False
  67. def __enter__(self):
  68. output = subprocess.check_output([GIT, 'stash'])
  69. self._stashed = 'No local changes' not in output
  70. # Get the original branch name or commit hash.
  71. self._orig_branch = subprocess.check_output([
  72. GIT, 'rev-parse', '--abbrev-ref', 'HEAD']).rstrip()
  73. if self._orig_branch == 'HEAD':
  74. self._orig_branch = subprocess.check_output([
  75. GIT, 'rev-parse', 'HEAD']).rstrip()
  76. # Check out a new branch, based at updated origin/master.
  77. subprocess.check_call([GIT, 'fetch', 'origin'])
  78. self._branch = '_tmp_%s' % uuid.uuid4()
  79. subprocess.check_call([GIT, 'checkout', '-b', self._branch,
  80. '-t', 'origin/master'])
  81. return self
  82. def __exit__(self, exc_type, _value, _traceback):
  83. subprocess.check_call([GIT, 'reset', '--hard', 'HEAD'])
  84. subprocess.check_call([GIT, 'checkout', self._orig_branch])
  85. if self._stashed:
  86. subprocess.check_call([GIT, 'stash', 'pop'])
  87. subprocess.check_call([GIT, 'branch', '-D', self._branch])
  88. def RemoveDirectory(*path):
  89. """Recursively removes a directory, even if it's marked read-only.
  90. This was copied from:
  91. https://chromium.googlesource.com/chromium/tools/build/+/f3e7ff03613cd59a463b2ccc49773c3813e77404/scripts/common/chromium_utils.py#491
  92. Remove the directory located at *path, if it exists.
  93. shutil.rmtree() doesn't work on Windows if any of the files or directories
  94. are read-only, which svn repositories and some .svn files are. We need to
  95. be able to force the files to be writable (i.e., deletable) as we traverse
  96. the tree.
  97. Even with all this, Windows still sometimes fails to delete a file, citing
  98. a permission error (maybe something to do with antivirus scans or disk
  99. indexing). The best suggestion any of the user forums had was to wait a
  100. bit and try again, so we do that too. It's hand-waving, but sometimes it
  101. works. :/
  102. """
  103. file_path = os.path.join(*path)
  104. if not os.path.exists(file_path):
  105. return
  106. if sys.platform == 'win32':
  107. # Give up and use cmd.exe's rd command.
  108. file_path = os.path.normcase(file_path)
  109. for _ in xrange(3):
  110. print 'RemoveDirectory running %s' % (' '.join(
  111. ['cmd.exe', '/c', 'rd', '/q', '/s', file_path]))
  112. if not subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', file_path]):
  113. break
  114. print ' Failed'
  115. time.sleep(3)
  116. return
  117. def RemoveWithRetry_non_win(rmfunc, path):
  118. if os.path.islink(path):
  119. return os.remove(path)
  120. else:
  121. return rmfunc(path)
  122. remove_with_retry = RemoveWithRetry_non_win
  123. def RmTreeOnError(function, path, excinfo):
  124. r"""This works around a problem whereby python 2.x on Windows has no ability
  125. to check for symbolic links. os.path.islink always returns False. But
  126. shutil.rmtree will fail if invoked on a symbolic link whose target was
  127. deleted before the link. E.g., reproduce like this:
  128. > mkdir test
  129. > mkdir test\1
  130. > mklink /D test\current test\1
  131. > python -c "import chromium_utils; chromium_utils.RemoveDirectory('test')"
  132. To avoid this issue, we pass this error-handling function to rmtree. If
  133. we see the exact sort of failure, we ignore it. All other failures we re-
  134. raise.
  135. """
  136. exception_type = excinfo[0]
  137. exception_value = excinfo[1]
  138. # If shutil.rmtree encounters a symbolic link on Windows, os.listdir will
  139. # fail with a WindowsError exception with an ENOENT errno (i.e., file not
  140. # found). We'll ignore that error. Note that WindowsError is not defined
  141. # for non-Windows platforms, so we use OSError (of which it is a subclass)
  142. # to avoid lint complaints about an undefined global on non-Windows
  143. # platforms.
  144. if (function is os.listdir) and issubclass(exception_type, OSError):
  145. if exception_value.errno == errno.ENOENT:
  146. # File does not exist, and we're trying to delete, so we can ignore the
  147. # failure.
  148. print 'WARNING: Failed to list %s during rmtree. Ignoring.\n' % path
  149. else:
  150. raise
  151. else:
  152. raise
  153. for root, dirs, files in os.walk(file_path, topdown=False):
  154. # For POSIX: making the directory writable guarantees removability.
  155. # Windows will ignore the non-read-only bits in the chmod value.
  156. os.chmod(root, 0770)
  157. for name in files:
  158. remove_with_retry(os.remove, os.path.join(root, name))
  159. for name in dirs:
  160. remove_with_retry(lambda p: shutil.rmtree(p, onerror=RmTreeOnError),
  161. os.path.join(root, name))
  162. remove_with_retry(os.rmdir, file_path)