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