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