| Index: tools/bisect_repackage/bisect_repackage_utils.py
|
| diff --git a/tools/bisect_repackage/bisect_repackage_utils.py b/tools/bisect_repackage/bisect_repackage_utils.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..b9825e8f7f3410507794f576afa8bf5b70280b9b
|
| --- /dev/null
|
| +++ b/tools/bisect_repackage/bisect_repackage_utils.py
|
| @@ -0,0 +1,502 @@
|
| +# Copyright (c) 2016 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +""" Set of basic operations/utilities that are used by repacakging builds
|
| +for bisect.
|
| +
|
| +These functions were mostly imported from build/scripts/common/chromium_utils
|
| +and build/scripts/common/slave_utils.
|
| +"""
|
| +
|
| +import os
|
| +import ast
|
| +import base64
|
| +import cStringIO
|
| +import copy
|
| +import errno
|
| +import fnmatch
|
| +import glob
|
| +import math
|
| +import multiprocessing
|
| +import os
|
| +import re
|
| +import shutil
|
| +import socket
|
| +import stat
|
| +import string # pylint: disable=W0402
|
| +import subprocess
|
| +import sys
|
| +import threading
|
| +import time
|
| +import traceback
|
| +import urllib
|
| +import zipfile
|
| +import zlib
|
| +
|
| +CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with '
|
| + 'no configured credentials')
|
| +
|
| +class ExternalError(Exception):
|
| + pass
|
| +def IsWindows():
|
| + return sys.platform == 'cygwin' or sys.platform.startswith('win')
|
| +
|
| +def IsLinux():
|
| + return sys.platform.startswith('linux')
|
| +
|
| +def IsMac():
|
| + return sys.platform.startswith('darwin')
|
| +
|
| +WIN_LINK_FUNC = None
|
| +
|
| +try:
|
| + if sys.platform.startswith('win'):
|
| + import ctypes
|
| + # There's 4 possibilities on Windows for links:
|
| + # 1. Symbolic file links;
|
| + # 2. Symbolic directory links;
|
| + # 3. Hardlinked files;
|
| + # 4. Junctioned directories.
|
| + # (Hardlinked directories don't really exist.)
|
| + #
|
| + # 7-Zip does not handle symbolic file links as we want (it puts the
|
| + # content of the link, not what it refers to, and reports "CRC Error" on
|
| + # extraction). It does work as expected for symbolic directory links.
|
| + # Because the majority of the large files are in the root of the staging
|
| + # directory, we do however need to handle file links, so we do this with
|
| + # hardlinking. Junctioning requires a huge whack of code, so we take the
|
| + # slightly odd tactic of using #2 and #3, but not #1 and #4. That is,
|
| + # hardlinks for files, but symbolic links for directories.
|
| + def _WIN_LINK_FUNC(src, dst):
|
| + print 'linking %s -> %s' % (src, dst)
|
| + if os.path.isdir(src):
|
| + if not ctypes.windll.kernel32.CreateSymbolicLinkA(
|
| + str(dst), str(os.path.abspath(src)), 1):
|
| + raise ctypes.WinError()
|
| + else:
|
| + if not ctypes.windll.kernel32.CreateHardLinkA(str(dst), str(src), 0):
|
| + raise ctypes.WinError()
|
| + WIN_LINK_FUNC = _WIN_LINK_FUNC
|
| +except ImportError:
|
| + # If we don't have ctypes or aren't on Windows, leave WIN_LINK_FUNC as None.
|
| + pass
|
| +
|
| +
|
| +
|
| +class PathNotFound(Exception):
|
| + pass
|
| +
|
| +
|
| +def isGitCommitHash(regex_match):
|
| + """Checks if match is correct SHA1 hash"""
|
| + matched_re = re.match(r"^[0-9,A-F]{40}$", regex_match.upper())
|
| + if matched_re: return True
|
| + return False
|
| +
|
| +def isCommitPosition(regex_match):
|
| + """Checks if match is correct revision(Cp number) format"""
|
| + matched_re = re.match(r"^[0-9]{6}$", regex_match)
|
| + if matched_re: return True
|
| + return False
|
| +
|
| +def MaybeMakeDirectory(*path):
|
| + """Creates an entire path, if it doesn't already exist."""
|
| + file_path = os.path.join(*path)
|
| + try:
|
| + os.makedirs(file_path)
|
| + except OSError, e:
|
| + if e.errno != errno.EEXIST:
|
| + raise
|
| +
|
| +def RemovePath(*path):
|
| + """Removes the file or directory at 'path', if it exists."""
|
| + file_path = os.path.join(*path)
|
| + if os.path.exists(file_path):
|
| + if os.path.isdir(file_path):
|
| + RemoveDirectory(file_path)
|
| + else:
|
| + RemoveFile(file_path)
|
| +
|
| +def MoveFile(path, new_path):
|
| + """Moves the file located at 'path' to 'new_path', if it exists."""
|
| + try:
|
| + RemoveFile(new_path)
|
| + os.rename(path, new_path)
|
| + except OSError, e:
|
| + if e.errno != errno.ENOENT:
|
| + raise
|
| +
|
| +def RemoveFile(*path):
|
| + """Removes the file located at 'path', if it exists."""
|
| + file_path = os.path.join(*path)
|
| + try:
|
| + os.remove(file_path)
|
| + except OSError, e:
|
| + if e.errno != errno.ENOENT:
|
| + raise
|
| +
|
| +def CheckDepotToolsInPath():
|
| + delimiter = ';' if sys.platform.startswith('win') else ':'
|
| + path_list = os.environ['PATH'].split(delimiter)
|
| + for path in path_list:
|
| + if path.rstrip(os.path.sep).endswith('depot_tools'):
|
| + return path
|
| + return None
|
| +
|
| +def RunGsutilCommand(args):
|
| + gsutil_path = CheckDepotToolsInPath()
|
| + if gsutil_path is None:
|
| + print ('Follow the instructions in this document '
|
| + 'http://dev.chromium.org/developers/how-tos/install-depot-tools'
|
| + ' to install depot_tools and then try again.')
|
| + sys.exit(1)
|
| + gsutil_path = os.path.join(gsutil_path, 'third_party', 'gsutil', 'gsutil')
|
| + gsutil = subprocess.Popen([sys.executable, gsutil_path] + args,
|
| + stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
| + env=None)
|
| + stdout, stderr = gsutil.communicate()
|
| + if gsutil.returncode:
|
| + if (re.findall(r'status[ |=]40[1|3]', stderr) or
|
| + stderr.startswith(CREDENTIAL_ERROR_MESSAGE)):
|
| + print ('Follow these steps to configure your credentials and try'
|
| + ' running the bisect-builds.py again.:\n'
|
| + ' 1. Run "python %s config" and follow its instructions.\n'
|
| + ' 2. If you have a @google.com account, use that account.\n'
|
| + ' 3. For the project-id, just enter 0.' % gsutil_path)
|
| + sys.exit(1)
|
| + else:
|
| + raise Exception('Error running the gsutil command: %s' % stderr)
|
| + return stdout
|
| +
|
| +def GSutilList(bucket):
|
| + query = '%s/' %(bucket)
|
| + stdout = RunGsutilCommand(['ls', query])
|
| + return [url[len(query):].strip('/') for url in stdout.splitlines()]
|
| +
|
| +def GSUtilDownloadFile(src, dst):
|
| + command = ['cp', src, dst]
|
| + return RunGsutilCommand(command)
|
| +
|
| +def GSUtilCopy(source, dest):
|
| + if not source.startswith('gs://') and not source.startswith('file://'):
|
| + source = 'file://' + source
|
| + if not dest.startswith('gs://') and not dest.startswith('file://'):
|
| + dest = 'file://' + dest
|
| + command = ['cp']
|
| + command.extend([source, dest])
|
| + return RunGsutilCommand(command)
|
| +
|
| +def RunCommand(cmd, cwd=None):
|
| + """Runs the given command and returns the exit code.
|
| +
|
| + Args:
|
| + cmd: list of command arguments.
|
| + cwd: working directory to execute the command, or None if the current
|
| + working directory should be used.
|
| +
|
| + Returns:
|
| + The exit code of the command.
|
| + """
|
| + process = subprocess.Popen(cmd, cwd=cwd)
|
| + process.wait()
|
| + return process.returncode
|
| +
|
| +
|
| +
|
| +def CopyFileToDir(src_path, dest_dir, dest_fn=None, link_ok=False):
|
| + """Copies the file found at src_path to the dest_dir directory, with metadata.
|
| +
|
| + If dest_fn is specified, the src_path is copied to that name in dest_dir,
|
| + otherwise it is copied to a file of the same name.
|
| +
|
| + Raises PathNotFound if either the file or the directory is not found.
|
| + """
|
| + # Verify the file and directory separately so we can tell them apart and
|
| + # raise PathNotFound rather than shutil.copyfile's IOError.
|
| + if not os.path.isfile(src_path):
|
| + raise PathNotFound('Unable to find file %s' % src_path)
|
| + if not os.path.isdir(dest_dir):
|
| + raise PathNotFound('Unable to find dir %s' % dest_dir)
|
| + src_file = os.path.basename(src_path)
|
| + if dest_fn:
|
| + # If we have ctypes and the caller doesn't mind links, use that to
|
| + # try to make the copy faster on Windows. http://crbug.com/418702.
|
| + if link_ok and WIN_LINK_FUNC:
|
| + WIN_LINK_FUNC(src_path, os.path.join(dest_dir, dest_fn))
|
| + else:
|
| + shutil.copy2(src_path, os.path.join(dest_dir, dest_fn))
|
| + else:
|
| + shutil.copy2(src_path, os.path.join(dest_dir, src_file))
|
| +
|
| +def RemoveDirectory(*path):
|
| + """Recursively removes a directory, even if it's marked read-only.
|
| +
|
| + Remove the directory located at *path, if it exists.
|
| +
|
| + shutil.rmtree() doesn't work on Windows if any of the files or directories
|
| + are read-only, which svn repositories and some .svn files are. We need to
|
| + be able to force the files to be writable (i.e., deletable) as we traverse
|
| + the tree.
|
| +
|
| + Even with all this, Windows still sometimes fails to delete a file, citing
|
| + a permission error (maybe something to do with antivirus scans or disk
|
| + indexing). The best suggestion any of the user forums had was to wait a
|
| + bit and try again, so we do that too. It's hand-waving, but sometimes it
|
| + works. :/
|
| + """
|
| + file_path = os.path.join(*path)
|
| + if not os.path.exists(file_path):
|
| + return
|
| +
|
| + if sys.platform == 'win32':
|
| + # Give up and use cmd.exe's rd command.
|
| + file_path = os.path.normcase(file_path)
|
| + for _ in xrange(3):
|
| + print 'RemoveDirectory running %s' % (' '.join(
|
| + ['cmd.exe', '/c', 'rd', '/q', '/s', file_path]))
|
| + if not subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', file_path]):
|
| + break
|
| + print ' Failed'
|
| + time.sleep(3)
|
| + return
|
| +
|
| + def RemoveWithRetry_non_win(rmfunc, path):
|
| + if os.path.islink(path):
|
| + return os.remove(path)
|
| + else:
|
| + return rmfunc(path)
|
| +
|
| + remove_with_retry = RemoveWithRetry_non_win
|
| +
|
| + def RmTreeOnError(function, path, excinfo):
|
| + r"""This works around a problem whereby python 2.x on Windows has no ability
|
| + to check for symbolic links. os.path.islink always returns False. But
|
| + shutil.rmtree will fail if invoked on a symbolic link whose target was
|
| + deleted before the link. E.g., reproduce like this:
|
| + > mkdir test
|
| + > mkdir test\1
|
| + > mklink /D test\current test\1
|
| + > python -c "import chromium_utils; chromium_utils.RemoveDirectory('test')"
|
| + To avoid this issue, we pass this error-handling function to rmtree. If
|
| + we see the exact sort of failure, we ignore it. All other failures we re-
|
| + raise.
|
| + """
|
| +
|
| + exception_type = excinfo[0]
|
| + exception_value = excinfo[1]
|
| + # If shutil.rmtree encounters a symbolic link on Windows, os.listdir will
|
| + # fail with a WindowsError exception with an ENOENT errno (i.e., file not
|
| + # found). We'll ignore that error. Note that WindowsError is not defined
|
| + # for non-Windows platforms, so we use OSError (of which it is a subclass)
|
| + # to avoid lint complaints about an undefined global on non-Windows
|
| + # platforms.
|
| + if (function is os.listdir) and issubclass(exception_type, OSError):
|
| + if exception_value.errno == errno.ENOENT:
|
| + # File does not exist, and we're trying to delete, so we can ignore the
|
| + # failure.
|
| + print 'WARNING: Failed to list %s during rmtree. Ignoring.\n' % path
|
| + else:
|
| + raise
|
| + else:
|
| + raise
|
| +
|
| + for root, dirs, files in os.walk(file_path, topdown=False):
|
| + # For POSIX: making the directory writable guarantees removability.
|
| + # Windows will ignore the non-read-only bits in the chmod value.
|
| + os.chmod(root, 0770)
|
| + for name in files:
|
| + remove_with_retry(os.remove, os.path.join(root, name))
|
| + for name in dirs:
|
| + remove_with_retry(lambda p: shutil.rmtree(p, onerror=RmTreeOnError),
|
| + os.path.join(root, name))
|
| +
|
| + remove_with_retry(os.rmdir, file_path)
|
| +
|
| +
|
| +
|
| +def MakeZip(output_dir, archive_name, file_list, file_relative_dir,
|
| + raise_error=True, remove_archive_directory=True, strip_files=None):
|
| + """Packs files into a new zip archive.
|
| +
|
| + Files are first copied into a directory within the output_dir named for
|
| + the archive_name, which will be created if necessary and emptied if it
|
| + already exists. The files are then then packed using archive names
|
| + relative to the output_dir. That is, if the zipfile is unpacked in place,
|
| + it will create a directory identical to the new archive_name directory, in
|
| + the output_dir. The zip file will be named as the archive_name, plus
|
| + '.zip'.
|
| +
|
| + Args:
|
| + output_dir: Absolute path to the directory in which the archive is to
|
| + be created.
|
| + archive_dir: Subdirectory of output_dir holding files to be added to
|
| + the new zipfile.
|
| + file_list: List of paths to files or subdirectories, relative to the
|
| + file_relative_dir.
|
| + file_relative_dir: Absolute path to the directory containing the files
|
| + and subdirectories in the file_list.
|
| + raise_error: Whether to raise a PathNotFound error if one of the files in
|
| + the list is not found.
|
| + remove_archive_directory: Whether to remove the archive staging directory
|
| + before copying files over to it.
|
| + strip_files: List of executable files to strip symbols when zipping
|
| +
|
| + Returns:
|
| + A tuple consisting of (archive_dir, zip_file_path), where archive_dir
|
| + is the full path to the newly created archive_name subdirectory.
|
| +
|
| + Raises:
|
| + PathNotFound if any of the files in the list is not found, unless
|
| + raise_error is False, in which case the error will be ignored.
|
| + """
|
| +
|
| + start_time = time.clock()
|
| + # Collect files into the archive directory.
|
| + archive_dir = os.path.join(output_dir, archive_name)
|
| + print 'output_dir: %s, archive_name: %s' % (output_dir, archive_name)
|
| + print 'archive_dir: %s, remove_archive_directory: %s, exists: %s' % (
|
| + archive_dir, remove_archive_directory, os.path.exists(archive_dir))
|
| + if remove_archive_directory and os.path.exists(archive_dir):
|
| + # Move it even if it's not a directory as expected. This can happen with
|
| + # FILES.cfg archive creation where we create an archive staging directory
|
| + # that is the same name as the ultimate archive name.
|
| + if not os.path.isdir(archive_dir):
|
| + print 'Moving old "%s" file to create same name directory.' % archive_dir
|
| + previous_archive_file = '%s.old' % archive_dir
|
| + MoveFile(archive_dir, previous_archive_file)
|
| + else:
|
| + print 'Removing %s' % archive_dir
|
| + RemoveDirectory(archive_dir)
|
| + print 'Now, os.path.exists(%s): %s' % (
|
| + archive_dir, os.path.exists(archive_dir))
|
| + MaybeMakeDirectory(archive_dir)
|
| + for needed_file in file_list:
|
| + needed_file = needed_file.rstrip()
|
| + # These paths are relative to the file_relative_dir. We need to copy
|
| + # them over maintaining the relative directories, where applicable.
|
| + src_path = os.path.join(file_relative_dir, needed_file)
|
| + dirname, basename = os.path.split(needed_file)
|
| + try:
|
| + if os.path.isdir(src_path):
|
| + if WIN_LINK_FUNC:
|
| + WIN_LINK_FUNC(src_path, os.path.join(archive_dir, needed_file))
|
| + else:
|
| + shutil.copytree(src_path, os.path.join(archive_dir, needed_file),
|
| + symlinks=True)
|
| + elif dirname != '' and basename != '':
|
| + dest_dir = os.path.join(archive_dir, dirname)
|
| + MaybeMakeDirectory(dest_dir)
|
| + CopyFileToDir(src_path, dest_dir, basename, link_ok=True)
|
| + if strip_files and basename in strip_files:
|
| + cmd = ['strip', os.path.join(dest_dir, basename)]
|
| + RunCommand(cmd)
|
| + else:
|
| + CopyFileToDir(src_path, archive_dir, basename, link_ok=True)
|
| + if strip_files and basename in strip_files:
|
| + cmd = ['strip', os.path.join(archive_dir, basename)]
|
| + RunCommand(cmd)
|
| + except PathNotFound:
|
| + if raise_error:
|
| + raise
|
| + end_time = time.clock()
|
| + print 'Took %f seconds to create archive directory.' % (end_time - start_time)
|
| +
|
| + # Pack the zip file.
|
| + output_file = '%s.zip' % archive_dir
|
| + previous_file = '%s_old.zip' % archive_dir
|
| + MoveFile(output_file, previous_file)
|
| +
|
| + # If we have 7z, use that as it's much faster. See http://crbug.com/418702.
|
| + windows_zip_cmd = None
|
| + if os.path.exists('C:\\Program Files\\7-Zip\\7z.exe'):
|
| + windows_zip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'a', '-y', '-mx1']
|
| +
|
| + # On Windows we use the python zip module; on Linux and Mac, we use the zip
|
| + # command as it will handle links and file bits (executable). Which is much
|
| + # easier then trying to do that with ZipInfo options.
|
| + start_time = time.clock()
|
| + if IsWindows() and not windows_zip_cmd:
|
| + print 'Creating %s' % output_file
|
| +
|
| + def _Addfiles(to_zip_file, dirname, files_to_add):
|
| + for this_file in files_to_add:
|
| + archive_name = this_file
|
| + this_path = os.path.join(dirname, this_file)
|
| + if os.path.isfile(this_path):
|
| + # Store files named relative to the outer output_dir.
|
| + archive_name = this_path.replace(output_dir + os.sep, '')
|
| + if os.path.getsize(this_path) == 0:
|
| + compress_method = zipfile.ZIP_STORED
|
| + else:
|
| + compress_method = zipfile.ZIP_DEFLATED
|
| + to_zip_file.write(this_path, archive_name, compress_method)
|
| + print 'Adding %s' % archive_name
|
| + zip_file = zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED,
|
| + allowZip64=True)
|
| + try:
|
| + os.path.walk(archive_dir, _Addfiles, zip_file)
|
| + finally:
|
| + zip_file.close()
|
| + else:
|
| + if IsMac() or IsLinux():
|
| + zip_cmd = ['zip', '-yr1']
|
| + else:
|
| + zip_cmd = windows_zip_cmd
|
| + saved_dir = os.getcwd()
|
| + os.chdir(os.path.dirname(archive_dir))
|
| + command = zip_cmd + [output_file, os.path.basename(archive_dir)]
|
| + result = RunCommand(command)
|
| + os.chdir(saved_dir)
|
| + if result and raise_error:
|
| + raise ExternalError('zip failed: %s => %s' %
|
| + (str(command), result))
|
| + end_time = time.clock()
|
| + print 'Took %f seconds to create zip.' % (end_time - start_time)
|
| + return (archive_dir, output_file)
|
| +
|
| +
|
| +def ExtractZip(filename, output_dir, verbose=True):
|
| + """ Extract the zip archive in the output directory.
|
| + """
|
| + MaybeMakeDirectory(output_dir)
|
| +
|
| + # On Linux and Mac, we use the unzip command as it will
|
| + # handle links and file bits (executable), which is much
|
| + # easier then trying to do that with ZipInfo options.
|
| + #
|
| + # The Mac Version of unzip unfortunately does not support Zip64, whereas
|
| + # the python module does, so we have to fallback to the python zip module
|
| + # on Mac if the filesize is greater than 4GB.
|
| + #
|
| + # On Windows, try to use 7z if it is installed, otherwise fall back to python
|
| + # zip module and pray we don't have files larger than 512MB to unzip.
|
| + unzip_cmd = None
|
| + if ((IsMac() and os.path.getsize(filename) < 4 * 1024 * 1024 * 1024)
|
| + or IsLinux()):
|
| + unzip_cmd = ['unzip', '-o']
|
| + elif IsWindows() and os.path.exists('C:\\Program Files\\7-Zip\\7z.exe'):
|
| + unzip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'x', '-y']
|
| +
|
| + if unzip_cmd:
|
| + # Make sure path is absolute before changing directories.
|
| + filepath = os.path.abspath(filename)
|
| + saved_dir = os.getcwd()
|
| + os.chdir(output_dir)
|
| + command = unzip_cmd + [filepath]
|
| + result = RunCommand(command)
|
| + os.chdir(saved_dir)
|
| + if result:
|
| + raise ExternalError('unzip failed: %s => %s' % (str(command), result))
|
| + else:
|
| + assert IsWindows() or IsMac()
|
| + zf = zipfile.ZipFile(filename)
|
| + # TODO(hinoka): This can be multiprocessed.
|
| + for name in zf.namelist():
|
| + if verbose:
|
| + print 'Extracting %s' % name
|
| + zf.extract(name, output_dir)
|
| + if IsMac():
|
| + # Restore permission bits.
|
| + os.chmod(os.path.join(output_dir, name),
|
| + zf.getinfo(name).external_attr >> 16L)
|
|
|