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