OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 """Set of basic operations/utilities that are used by repacakging tool. |
| 6 |
| 7 These functions were mostly imported from build/scripts/common/chromium_utils |
| 8 and build/scripts/common/slave_utils. |
| 9 """ |
| 10 |
| 11 import errno |
| 12 import os |
| 13 import re |
| 14 import shutil |
| 15 import subprocess |
| 16 import sys |
| 17 import time |
| 18 import zipfile |
| 19 |
| 20 |
| 21 CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with ' |
| 22 'no configured credentials') |
| 23 |
| 24 |
| 25 class ExternalError(Exception): |
| 26 pass |
| 27 |
| 28 |
| 29 def IsWindows(): |
| 30 return sys.platform == 'cygwin' or sys.platform.startswith('win') |
| 31 |
| 32 |
| 33 def IsLinux(): |
| 34 return sys.platform.startswith('linux') |
| 35 |
| 36 |
| 37 def IsMac(): |
| 38 return sys.platform.startswith('darwin') |
| 39 |
| 40 WIN_LINK_FUNC = None |
| 41 |
| 42 try: |
| 43 if sys.platform.startswith('win'): |
| 44 import ctypes |
| 45 # There's 4 possibilities on Windows for links: |
| 46 # 1. Symbolic file links; |
| 47 # 2. Symbolic directory links; |
| 48 # 3. Hardlinked files; |
| 49 # 4. Junctioned directories. |
| 50 # (Hardlinked directories don't really exist.) |
| 51 # |
| 52 # 7-Zip does not handle symbolic file links as we want (it puts the |
| 53 # content of the link, not what it refers to, and reports "CRC Error" on |
| 54 # extraction). It does work as expected for symbolic directory links. |
| 55 # Because the majority of the large files are in the root of the staging |
| 56 # directory, we do however need to handle file links, so we do this with |
| 57 # hardlinking. Junctioning requires a huge whack of code, so we take the |
| 58 # slightly odd tactic of using #2 and #3, but not #1 and #4. That is, |
| 59 # hardlinks for files, but symbolic links for directories. |
| 60 def _WIN_LINK_FUNC(src, dst): |
| 61 print 'linking %s -> %s' % (src, dst) |
| 62 if os.path.isdir(src): |
| 63 if not ctypes.windll.kernel32.CreateSymbolicLinkA( |
| 64 str(dst), str(os.path.abspath(src)), 1): |
| 65 raise ctypes.WinError() |
| 66 else: |
| 67 if not ctypes.windll.kernel32.CreateHardLinkA(str(dst), str(src), 0): |
| 68 raise ctypes.WinError() |
| 69 WIN_LINK_FUNC = _WIN_LINK_FUNC |
| 70 except ImportError: |
| 71 # If we don't have ctypes or aren't on Windows, leave WIN_LINK_FUNC as None. |
| 72 pass |
| 73 |
| 74 |
| 75 class PathNotFound(Exception): |
| 76 pass |
| 77 |
| 78 |
| 79 def IsGitCommitHash(regex_match): |
| 80 """Checks if match is correct SHA1 hash.""" |
| 81 matched_re = re.match(r'^[0-9,A-F]{40}$', regex_match.upper()) |
| 82 if matched_re: return True |
| 83 return False |
| 84 |
| 85 |
| 86 def IsCommitPosition(regex_match): |
| 87 """Checks if match is correct revision(Cp number) format.""" |
| 88 matched_re = re.match(r'^[0-9]{6}$', regex_match) |
| 89 if matched_re: return True |
| 90 return False |
| 91 |
| 92 |
| 93 def MaybeMakeDirectory(*path): |
| 94 """Creates an entire path, if it doesn't already exist.""" |
| 95 file_path = os.path.join(*path) |
| 96 try: |
| 97 os.makedirs(file_path) |
| 98 except OSError, e: |
| 99 if e.errno != errno.EEXIST: |
| 100 raise |
| 101 |
| 102 |
| 103 def RemovePath(*path): |
| 104 """Removes the file or directory at 'path', if it exists.""" |
| 105 file_path = os.path.join(*path) |
| 106 if os.path.exists(file_path): |
| 107 if os.path.isdir(file_path): |
| 108 RemoveDirectory(file_path) |
| 109 else: |
| 110 RemoveFile(file_path) |
| 111 |
| 112 |
| 113 def MoveFile(path, new_path): |
| 114 """Moves the file located at 'path' to 'new_path', if it exists.""" |
| 115 try: |
| 116 RemoveFile(new_path) |
| 117 os.rename(path, new_path) |
| 118 except OSError, e: |
| 119 if e.errno != errno.ENOENT: |
| 120 raise |
| 121 |
| 122 |
| 123 def RemoveFile(*path): |
| 124 """Removes the file located at 'path', if it exists.""" |
| 125 file_path = os.path.join(*path) |
| 126 try: |
| 127 os.remove(file_path) |
| 128 except OSError, e: |
| 129 if e.errno != errno.ENOENT: |
| 130 raise |
| 131 |
| 132 |
| 133 def CheckDepotToolsInPath(): |
| 134 delimiter = ';' if sys.platform.startswith('win') else ':' |
| 135 path_list = os.environ['PATH'].split(delimiter) |
| 136 for path in path_list: |
| 137 if path.rstrip(os.path.sep).endswith('depot_tools'): |
| 138 return path |
| 139 return None |
| 140 |
| 141 |
| 142 def RunGsutilCommand(args): |
| 143 gsutil_path = CheckDepotToolsInPath() |
| 144 if gsutil_path is None: |
| 145 print ('Follow the instructions in this document ' |
| 146 'http://dev.chromium.org/developers/how-tos/install-depot-tools' |
| 147 ' to install depot_tools and then try again.') |
| 148 sys.exit(1) |
| 149 gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil') |
| 150 gsutil = subprocess.Popen([sys.executable, gsutil_path] + args, |
| 151 stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
| 152 env=None) |
| 153 stdout, stderr = gsutil.communicate() |
| 154 if gsutil.returncode: |
| 155 if (re.findall(r'status[ |=]40[1|3]', stderr) or |
| 156 stderr.startswith(CREDENTIAL_ERROR_MESSAGE)): |
| 157 print ('Follow these steps to configure your credentials and try' |
| 158 ' running the bisect-builds.py again.:\n' |
| 159 ' 1. Run "python %s config" and follow its instructions.\n' |
| 160 ' 2. If you have a @google.com account, use that account.\n' |
| 161 ' 3. For the project-id, just enter 0.' % gsutil_path) |
| 162 sys.exit(1) |
| 163 else: |
| 164 raise Exception('Error running the gsutil command: %s' % stderr) |
| 165 return stdout |
| 166 |
| 167 |
| 168 def GSutilList(bucket): |
| 169 query = '%s/' %(bucket) |
| 170 stdout = RunGsutilCommand(['ls', query]) |
| 171 return [url[len(query):].strip('/') for url in stdout.splitlines()] |
| 172 |
| 173 |
| 174 def GSUtilDownloadFile(src, dst): |
| 175 command = ['cp', src, dst] |
| 176 return RunGsutilCommand(command) |
| 177 |
| 178 |
| 179 def GSUtilCopy(source, dest): |
| 180 if not source.startswith('gs://') and not source.startswith('file://'): |
| 181 source = 'file://' + source |
| 182 if not dest.startswith('gs://') and not dest.startswith('file://'): |
| 183 dest = 'file://' + dest |
| 184 command = ['cp'] |
| 185 command.extend([source, dest]) |
| 186 return RunGsutilCommand(command) |
| 187 |
| 188 |
| 189 def RunCommand(cmd, cwd=None): |
| 190 """Runs the given command and returns the exit code. |
| 191 |
| 192 Args: |
| 193 cmd: list of command arguments. |
| 194 cwd: working directory to execute the command, or None if the current |
| 195 working directory should be used. |
| 196 |
| 197 Returns: |
| 198 The exit code of the command. |
| 199 """ |
| 200 process = subprocess.Popen(cmd, cwd=cwd) |
| 201 process.wait() |
| 202 return process.returncode |
| 203 |
| 204 |
| 205 def CopyFileToDir(src_path, dest_dir, dest_fn=None, link_ok=False): |
| 206 """Copies the file found at src_path to the dest_dir directory, with metadata. |
| 207 |
| 208 If dest_fn is specified, the src_path is copied to that name in dest_dir, |
| 209 otherwise it is copied to a file of the same name. |
| 210 |
| 211 Raises PathNotFound if either the file or the directory is not found. |
| 212 """ |
| 213 # Verify the file and directory separately so we can tell them apart and |
| 214 # raise PathNotFound rather than shutil.copyfile's IOError. |
| 215 if not os.path.isfile(src_path): |
| 216 raise PathNotFound('Unable to find file %s' % src_path) |
| 217 if not os.path.isdir(dest_dir): |
| 218 raise PathNotFound('Unable to find dir %s' % dest_dir) |
| 219 src_file = os.path.basename(src_path) |
| 220 if dest_fn: |
| 221 # If we have ctypes and the caller doesn't mind links, use that to |
| 222 # try to make the copy faster on Windows. http://crbug.com/418702. |
| 223 if link_ok and WIN_LINK_FUNC: |
| 224 WIN_LINK_FUNC(src_path, os.path.join(dest_dir, dest_fn)) |
| 225 else: |
| 226 shutil.copy2(src_path, os.path.join(dest_dir, dest_fn)) |
| 227 else: |
| 228 shutil.copy2(src_path, os.path.join(dest_dir, src_file)) |
| 229 |
| 230 |
| 231 def RemoveDirectory(*path): |
| 232 """Recursively removes a directory, even if it's marked read-only. |
| 233 |
| 234 Remove the directory located at *path, if it exists. |
| 235 |
| 236 shutil.rmtree() doesn't work on Windows if any of the files or directories |
| 237 are read-only, which svn repositories and some .svn files are. We need to |
| 238 be able to force the files to be writable (i.e., deletable) as we traverse |
| 239 the tree. |
| 240 |
| 241 Even with all this, Windows still sometimes fails to delete a file, citing |
| 242 a permission error (maybe something to do with antivirus scans or disk |
| 243 indexing). The best suggestion any of the user forums had was to wait a |
| 244 bit and try again, so we do that too. It's hand-waving, but sometimes it |
| 245 works. :/ |
| 246 """ |
| 247 file_path = os.path.join(*path) |
| 248 if not os.path.exists(file_path): |
| 249 return |
| 250 |
| 251 if sys.platform == 'win32': |
| 252 # Give up and use cmd.exe's rd command. |
| 253 file_path = os.path.normcase(file_path) |
| 254 for _ in xrange(3): |
| 255 print 'RemoveDirectory running %s' % (' '.join( |
| 256 ['cmd.exe', '/c', 'rd', '/q', '/s', file_path])) |
| 257 if not subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', file_path]): |
| 258 break |
| 259 print ' Failed' |
| 260 time.sleep(3) |
| 261 return |
| 262 |
| 263 def RemoveWithRetry_non_win(rmfunc, path): |
| 264 if os.path.islink(path): |
| 265 return os.remove(path) |
| 266 else: |
| 267 return rmfunc(path) |
| 268 |
| 269 remove_with_retry = RemoveWithRetry_non_win |
| 270 |
| 271 def RmTreeOnError(function, path, excinfo): |
| 272 r"""This works around a problem whereby python 2.x on Windows has no ability |
| 273 to check for symbolic links. os.path.islink always returns False. But |
| 274 shutil.rmtree will fail if invoked on a symbolic link whose target was |
| 275 deleted before the link. E.g., reproduce like this: |
| 276 > mkdir test |
| 277 > mkdir test\1 |
| 278 > mklink /D test\current test\1 |
| 279 > python -c "import chromium_utils; chromium_utils.RemoveDirectory('test')" |
| 280 To avoid this issue, we pass this error-handling function to rmtree. If |
| 281 we see the exact sort of failure, we ignore it. All other failures we re- |
| 282 raise. |
| 283 """ |
| 284 |
| 285 exception_type = excinfo[0] |
| 286 exception_value = excinfo[1] |
| 287 # If shutil.rmtree encounters a symbolic link on Windows, os.listdir will |
| 288 # fail with a WindowsError exception with an ENOENT errno (i.e., file not |
| 289 # found). We'll ignore that error. Note that WindowsError is not defined |
| 290 # for non-Windows platforms, so we use OSError (of which it is a subclass) |
| 291 # to avoid lint complaints about an undefined global on non-Windows |
| 292 # platforms. |
| 293 if (function is os.listdir) and issubclass(exception_type, OSError): |
| 294 if exception_value.errno == errno.ENOENT: |
| 295 # File does not exist, and we're trying to delete, so we can ignore the |
| 296 # failure. |
| 297 print 'WARNING: Failed to list %s during rmtree. Ignoring.\n' % path |
| 298 else: |
| 299 raise |
| 300 else: |
| 301 raise |
| 302 |
| 303 for root, dirs, files in os.walk(file_path, topdown=False): |
| 304 # For POSIX: making the directory writable guarantees removability. |
| 305 # Windows will ignore the non-read-only bits in the chmod value. |
| 306 os.chmod(root, 0770) |
| 307 for name in files: |
| 308 remove_with_retry(os.remove, os.path.join(root, name)) |
| 309 for name in dirs: |
| 310 remove_with_retry(lambda p: shutil.rmtree(p, onerror=RmTreeOnError), |
| 311 os.path.join(root, name)) |
| 312 |
| 313 remove_with_retry(os.rmdir, file_path) |
| 314 |
| 315 |
| 316 def MakeZip(output_dir, archive_name, file_list, file_relative_dir, |
| 317 raise_error=True, remove_archive_directory=True, strip_files=None): |
| 318 """Packs files into a new zip archive. |
| 319 |
| 320 Files are first copied into a directory within the output_dir named for |
| 321 the archive_name, which will be created if necessary and emptied if it |
| 322 already exists. The files are then then packed using archive names |
| 323 relative to the output_dir. That is, if the zipfile is unpacked in place, |
| 324 it will create a directory identical to the new archive_name directory, in |
| 325 the output_dir. The zip file will be named as the archive_name, plus |
| 326 '.zip'. |
| 327 |
| 328 Args: |
| 329 output_dir: Absolute path to the directory in which the archive is to |
| 330 be created. |
| 331 archive_dir: Subdirectory of output_dir holding files to be added to |
| 332 the new zipfile. |
| 333 file_list: List of paths to files or subdirectories, relative to the |
| 334 file_relative_dir. |
| 335 file_relative_dir: Absolute path to the directory containing the files |
| 336 and subdirectories in the file_list. |
| 337 raise_error: Whether to raise a PathNotFound error if one of the files in |
| 338 the list is not found. |
| 339 remove_archive_directory: Whether to remove the archive staging directory |
| 340 before copying files over to it. |
| 341 strip_files: List of executable files to strip symbols when zipping |
| 342 |
| 343 Returns: |
| 344 A tuple consisting of (archive_dir, zip_file_path), where archive_dir |
| 345 is the full path to the newly created archive_name subdirectory. |
| 346 |
| 347 Raises: |
| 348 PathNotFound if any of the files in the list is not found, unless |
| 349 raise_error is False, in which case the error will be ignored. |
| 350 """ |
| 351 |
| 352 start_time = time.clock() |
| 353 # Collect files into the archive directory. |
| 354 archive_dir = os.path.join(output_dir, archive_name) |
| 355 print 'output_dir: %s, archive_name: %s' % (output_dir, archive_name) |
| 356 print 'archive_dir: %s, remove_archive_directory: %s, exists: %s' % ( |
| 357 archive_dir, remove_archive_directory, os.path.exists(archive_dir)) |
| 358 if remove_archive_directory and os.path.exists(archive_dir): |
| 359 # Move it even if it's not a directory as expected. This can happen with |
| 360 # FILES.cfg archive creation where we create an archive staging directory |
| 361 # that is the same name as the ultimate archive name. |
| 362 if not os.path.isdir(archive_dir): |
| 363 print 'Moving old "%s" file to create same name directory.' % archive_dir |
| 364 previous_archive_file = '%s.old' % archive_dir |
| 365 MoveFile(archive_dir, previous_archive_file) |
| 366 else: |
| 367 print 'Removing %s' % archive_dir |
| 368 RemoveDirectory(archive_dir) |
| 369 print 'Now, os.path.exists(%s): %s' % ( |
| 370 archive_dir, os.path.exists(archive_dir)) |
| 371 MaybeMakeDirectory(archive_dir) |
| 372 for needed_file in file_list: |
| 373 needed_file = needed_file.rstrip() |
| 374 # These paths are relative to the file_relative_dir. We need to copy |
| 375 # them over maintaining the relative directories, where applicable. |
| 376 src_path = os.path.join(file_relative_dir, needed_file) |
| 377 dirname, basename = os.path.split(needed_file) |
| 378 try: |
| 379 if os.path.isdir(src_path): |
| 380 if WIN_LINK_FUNC: |
| 381 WIN_LINK_FUNC(src_path, os.path.join(archive_dir, needed_file)) |
| 382 else: |
| 383 shutil.copytree(src_path, os.path.join(archive_dir, needed_file), |
| 384 symlinks=True) |
| 385 elif dirname != '' and basename != '': |
| 386 dest_dir = os.path.join(archive_dir, dirname) |
| 387 MaybeMakeDirectory(dest_dir) |
| 388 CopyFileToDir(src_path, dest_dir, basename, link_ok=True) |
| 389 if strip_files and basename in strip_files: |
| 390 cmd = ['strip', os.path.join(dest_dir, basename)] |
| 391 RunCommand(cmd) |
| 392 else: |
| 393 CopyFileToDir(src_path, archive_dir, basename, link_ok=True) |
| 394 if strip_files and basename in strip_files: |
| 395 cmd = ['strip', os.path.join(archive_dir, basename)] |
| 396 RunCommand(cmd) |
| 397 except PathNotFound: |
| 398 if raise_error: |
| 399 raise |
| 400 end_time = time.clock() |
| 401 print 'Took %f seconds to create archive directory.' % (end_time - start_time) |
| 402 |
| 403 # Pack the zip file. |
| 404 output_file = '%s.zip' % archive_dir |
| 405 previous_file = '%s_old.zip' % archive_dir |
| 406 MoveFile(output_file, previous_file) |
| 407 |
| 408 # If we have 7z, use that as it's much faster. See http://crbug.com/418702. |
| 409 windows_zip_cmd = None |
| 410 if os.path.exists('C:\\Program Files\\7-Zip\\7z.exe'): |
| 411 windows_zip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'a', '-y', '-mx1'] |
| 412 |
| 413 # On Windows we use the python zip module; on Linux and Mac, we use the zip |
| 414 # command as it will handle links and file bits (executable). Which is much |
| 415 # easier then trying to do that with ZipInfo options. |
| 416 start_time = time.clock() |
| 417 if IsWindows() and not windows_zip_cmd: |
| 418 print 'Creating %s' % output_file |
| 419 |
| 420 def _Addfiles(to_zip_file, dirname, files_to_add): |
| 421 for this_file in files_to_add: |
| 422 archive_name = this_file |
| 423 this_path = os.path.join(dirname, this_file) |
| 424 if os.path.isfile(this_path): |
| 425 # Store files named relative to the outer output_dir. |
| 426 archive_name = this_path.replace(output_dir + os.sep, '') |
| 427 if os.path.getsize(this_path) == 0: |
| 428 compress_method = zipfile.ZIP_STORED |
| 429 else: |
| 430 compress_method = zipfile.ZIP_DEFLATED |
| 431 to_zip_file.write(this_path, archive_name, compress_method) |
| 432 print 'Adding %s' % archive_name |
| 433 zip_file = zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED, |
| 434 allowZip64=True) |
| 435 try: |
| 436 os.path.walk(archive_dir, _Addfiles, zip_file) |
| 437 finally: |
| 438 zip_file.close() |
| 439 else: |
| 440 if IsMac() or IsLinux(): |
| 441 zip_cmd = ['zip', '-yr1'] |
| 442 else: |
| 443 zip_cmd = windows_zip_cmd |
| 444 saved_dir = os.getcwd() |
| 445 os.chdir(os.path.dirname(archive_dir)) |
| 446 command = zip_cmd + [output_file, os.path.basename(archive_dir)] |
| 447 result = RunCommand(command) |
| 448 os.chdir(saved_dir) |
| 449 if result and raise_error: |
| 450 raise ExternalError('zip failed: %s => %s' % |
| 451 (str(command), result)) |
| 452 end_time = time.clock() |
| 453 print 'Took %f seconds to create zip.' % (end_time - start_time) |
| 454 return (archive_dir, output_file) |
| 455 |
| 456 |
| 457 def ExtractZip(filename, output_dir, verbose=True): |
| 458 """Extract the zip archive in the output directory.""" |
| 459 MaybeMakeDirectory(output_dir) |
| 460 |
| 461 # On Linux and Mac, we use the unzip command as it will |
| 462 # handle links and file bits (executable), which is much |
| 463 # easier then trying to do that with ZipInfo options. |
| 464 # |
| 465 # The Mac Version of unzip unfortunately does not support Zip64, whereas |
| 466 # the python module does, so we have to fallback to the python zip module |
| 467 # on Mac if the filesize is greater than 4GB. |
| 468 # |
| 469 # On Windows, try to use 7z if it is installed, otherwise fall back to python |
| 470 # zip module and pray we don't have files larger than 512MB to unzip. |
| 471 unzip_cmd = None |
| 472 if ((IsMac() and os.path.getsize(filename) < 4 * 1024 * 1024 * 1024) |
| 473 or IsLinux()): |
| 474 unzip_cmd = ['unzip', '-o'] |
| 475 elif IsWindows() and os.path.exists('C:\\Program Files\\7-Zip\\7z.exe'): |
| 476 unzip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'x', '-y'] |
| 477 |
| 478 if unzip_cmd: |
| 479 # Make sure path is absolute before changing directories. |
| 480 filepath = os.path.abspath(filename) |
| 481 saved_dir = os.getcwd() |
| 482 os.chdir(output_dir) |
| 483 command = unzip_cmd + [filepath] |
| 484 result = RunCommand(command) |
| 485 os.chdir(saved_dir) |
| 486 if result: |
| 487 raise ExternalError('unzip failed: %s => %s' % (str(command), result)) |
| 488 else: |
| 489 assert IsWindows() or IsMac() |
| 490 zf = zipfile.ZipFile(filename) |
| 491 # TODO(hinoka): This can be multiprocessed. |
| 492 for name in zf.namelist(): |
| 493 if verbose: |
| 494 print 'Extracting %s' % name |
| 495 zf.extract(name, output_dir) |
| 496 if IsMac(): |
| 497 # Restore permission bits. |
| 498 os.chmod(os.path.join(output_dir, name), |
| 499 zf.getinfo(name).external_attr >> 16L) |
OLD | NEW |