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