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