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