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