| #!/usr/bin/env python |
| # Copyright 2013 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. |
| |
| """Downloads and unpacks a toolchain for building on Windows. The contents are |
| matched by sha1 which will be updated when the toolchain is updated. |
| |
| Having a toolchain script in depot_tools means that it's not versioned |
| directly with the source code. That is, if the toolchain is upgraded, but |
| you're trying to build an historical version of Chromium from before the |
| toolchain upgrade, this will cause you to build with a newer toolchain than |
| was available when that code was committed. This is done for a two main |
| reasons: 1) it would likely be annoying to have the up-to-date toolchain |
| removed and replaced by one without a service pack applied); 2) it would |
| require maintaining scripts that can build older not-up-to-date revisions of |
| the toolchain. This is likely to be a poorly tested code path that probably |
| won't be properly maintained. See http://crbug.com/323300. |
| |
| This does not extend to major versions of the toolchain however, on the |
| assumption that there are more likely to be source incompatibilities between |
| major revisions. This script calls a subscript (currently, toolchain2013.py) |
| to do the main work. It is expected that toolchain2013.py will always be able |
| to acquire/build the most current revision of a VS2013-based toolchain. In the |
| future when a hypothetical VS2015 is released, the 2013 script will be |
| maintained, and a new 2015 script would be added. |
| """ |
| |
| from __future__ import print_function |
| |
| import hashlib |
| import json |
| import optparse |
| import os |
| import platform |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| import zipfile |
| |
| # Environment variable that, if set, specifies the default Visual Studio |
| # toolchain root directory to use. |
| ENV_TOOLCHAIN_ROOT = 'DEPOT_TOOLS_WIN_TOOLCHAIN_ROOT' |
| |
| # winreg isn't natively available under CygWin |
| if sys.platform == "win32": |
| try: |
| import winreg |
| except ImportError: |
| import _winreg as winreg |
| elif sys.platform == "cygwin": |
| try: |
| import cygwinreg as winreg |
| except ImportError: |
| print('') |
| print('CygWin does not natively support winreg but a replacement exists.') |
| print('https://pypi.python.org/pypi/cygwinreg/') |
| print('') |
| print('Try: easy_install cygwinreg') |
| print('') |
| raise |
| |
| BASEDIR = os.path.dirname(os.path.abspath(__file__)) |
| DEPOT_TOOLS_PATH = os.path.join(BASEDIR, '..') |
| sys.path.append(DEPOT_TOOLS_PATH) |
| try: |
| import download_from_google_storage |
| except ImportError: |
| # Allow use of utility functions in this script from package_from_installed |
| # on bare VM that doesn't have a full depot_tools. |
| pass |
| |
| |
| def GetFileList(root): |
| """Gets a normalized list of files under |root|.""" |
| assert not os.path.isabs(root) |
| assert os.path.normpath(root) == root |
| file_list = [] |
| # Ignore WER ReportQueue entries that vctip/cl leave in the bin dir if/when |
| # they crash. Also ignores the content of the |
| # win_sdk/debuggers/x(86|64)/(sym|src)/ directories as this is just the |
| # temporarily location that Windbg might use to store the symbol files and |
| # downloaded sources. |
| # |
| # Note: These files are only created on a Windows host, so the |
| # ignored_directories list isn't relevant on non-Windows hosts. |
| |
| ignored_directories = ['wer\\reportqueue', |
| 'win_sdk\\debuggers\\x86\\sym\\', |
| 'win_sdk\\debuggers\\x64\\sym\\', |
| 'win_sdk\\debuggers\\x86\\src\\', |
| 'win_sdk\\debuggers\\x64\\src\\'] |
| for base, _, files in os.walk(root): |
| paths = [os.path.join(base, f) for f in files] |
| for p in paths: |
| if any(ignored_dir in p.lower() for ignored_dir in ignored_directories): |
| continue |
| file_list.append(p) |
| return sorted(file_list, key=lambda s: s.replace('/', '\\').lower()) |
| |
| |
| def MakeTimestampsFileName(root, sha1): |
| return os.path.join(root, os.pardir, '%s.timestamps' % sha1) |
| |
| |
| def CalculateHash(root, expected_hash): |
| """Calculates the sha1 of the paths to all files in the given |root| and the |
| contents of those files, and returns as a hex string. |
| |
| |expected_hash| is the expected hash value for this toolchain if it has |
| already been installed. |
| """ |
| if expected_hash: |
| full_root_path = os.path.join(root, expected_hash) |
| else: |
| full_root_path = root |
| file_list = GetFileList(full_root_path) |
| # Check whether we previously saved timestamps in $root/../{sha1}.timestamps. |
| # If we didn't, or they don't match, then do the full calculation, otherwise |
| # return the saved value. |
| timestamps_file = MakeTimestampsFileName(root, expected_hash) |
| timestamps_data = {'files': [], 'sha1': ''} |
| if os.path.exists(timestamps_file): |
| with open(timestamps_file, 'rb') as f: |
| try: |
| timestamps_data = json.load(f) |
| except ValueError: |
| # json couldn't be loaded, empty data will force a re-hash. |
| pass |
| |
| matches = len(file_list) == len(timestamps_data['files']) |
| # Don't check the timestamp of the version file as we touch this file to |
| # indicates which versions of the toolchain are still being used. |
| vc_dir = os.path.join(full_root_path, 'VC').lower() |
| if matches: |
| for disk, cached in zip(file_list, timestamps_data['files']): |
| if disk != cached[0] or ( |
| disk != vc_dir and os.path.getmtime(disk) != cached[1]): |
| matches = False |
| break |
| elif os.path.exists(timestamps_file): |
| # Print some information about the extra/missing files. Don't do this if we |
| # don't have a timestamp file, as all the files will be considered as |
| # missing. |
| timestamps_data_files = [] |
| for f in timestamps_data['files']: |
| timestamps_data_files.append(f[0]) |
| missing_files = [f for f in timestamps_data_files if f not in file_list] |
| if len(missing_files): |
| print('%d files missing from the %s version of the toolchain:' % |
| (len(missing_files), expected_hash)) |
| for f in missing_files[:10]: |
| print('\t%s' % f) |
| if len(missing_files) > 10: |
| print('\t...') |
| extra_files = [f for f in file_list if f not in timestamps_data_files] |
| if len(extra_files): |
| print('%d extra files in the %s version of the toolchain:' % |
| (len(extra_files), expected_hash)) |
| for f in extra_files[:10]: |
| print('\t%s' % f) |
| if len(extra_files) > 10: |
| print('\t...') |
| if matches: |
| return timestamps_data['sha1'] |
| |
| # Make long hangs when updating the toolchain less mysterious. |
| print('Calculating hash of toolchain in %s. Please wait...' % full_root_path) |
| sys.stdout.flush() |
| digest = hashlib.sha1() |
| for path in file_list: |
| path_without_hash = str(path).replace('/', '\\') |
| if expected_hash: |
| path_without_hash = path_without_hash.replace( |
| os.path.join(root, expected_hash).replace('/', '\\'), root) |
| if sys.version_info[0] < 3: |
| digest.update(path_without_hash.lower()) |
| else: |
| digest.update(bytes(path_without_hash.lower(), 'utf-8')) |
| with open(path, 'rb') as f: |
| digest.update(f.read()) |
| |
| # Save the timestamp file if the calculated hash is the expected one. |
| if digest.hexdigest() == expected_hash: |
| SaveTimestampsAndHash(root, digest.hexdigest()) |
| return digest.hexdigest() |
| |
| |
| def CalculateToolchainHashes(root, remove_corrupt_toolchains): |
| """Calculate the hash of the different toolchains installed in the |root| |
| directory.""" |
| hashes = [] |
| dir_list = [ |
| d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))] |
| for d in dir_list: |
| toolchain_hash = CalculateHash(root, d) |
| if toolchain_hash != d: |
| print('The hash of a version of the toolchain has an unexpected value (' |
| '%s instead of %s)%s.' % (toolchain_hash, d, |
| ', removing it' if remove_corrupt_toolchains else '')) |
| if remove_corrupt_toolchains: |
| RemoveToolchain(root, d, True) |
| else: |
| hashes.append(toolchain_hash) |
| return hashes |
| |
| |
| def SaveTimestampsAndHash(root, sha1): |
| """Saves timestamps and the final hash to be able to early-out more quickly |
| next time.""" |
| file_list = GetFileList(os.path.join(root, sha1)) |
| timestamps_data = { |
| 'files': [[f, os.path.getmtime(f)] for f in file_list], |
| 'sha1': sha1, |
| } |
| with open(MakeTimestampsFileName(root, sha1), 'wb') as f: |
| json.dump(timestamps_data, f) |
| |
| |
| def HaveSrcInternalAccess(): |
| """Checks whether access to src-internal is available.""" |
| with open(os.devnull, 'w') as nul: |
| # This is required to avoid modal dialog boxes after Git 2.14.1 and Git |
| # Credential Manager for Windows 1.12. See https://crbug.com/755694 and |
| # https://github.com/Microsoft/Git-Credential-Manager-for-Windows/issues/482. |
| child_env = dict(os.environ, GCM_INTERACTIVE='NEVER') |
| return subprocess.call( |
| ['git', '-c', 'core.askpass=true', 'remote', 'show', |
| 'https://chrome-internal.googlesource.com/chrome/src-internal/'], |
| shell=True, stdin=nul, stdout=nul, stderr=nul, env=child_env) == 0 |
| |
| |
| def LooksLikeGoogler(): |
| """Checks for a USERDOMAIN environment variable of 'GOOGLE', which |
| probably implies the current user is a Googler.""" |
| return os.environ.get('USERDOMAIN', '').upper() == 'GOOGLE' |
| |
| |
| def CanAccessToolchainBucket(): |
| """Checks whether the user has access to gs://chrome-wintoolchain/.""" |
| gsutil = download_from_google_storage.Gsutil( |
| download_from_google_storage.GSUTIL_DEFAULT_PATH, boto_path=None) |
| code, _, _ = gsutil.check_call('ls', 'gs://chrome-wintoolchain/') |
| return code == 0 |
| |
| |
| def ToolchainBaseURL(): |
| base_url = os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN_BASE_URL', '') |
| if base_url.startswith('file://'): |
| base_url = base_url[len('file://'):] |
| return base_url |
| |
| |
| def UsesToolchainFromFile(): |
| return os.path.isdir(ToolchainBaseURL()) |
| |
| |
| def UsesToolchainFromHttp(): |
| url = ToolchainBaseURL() |
| return url.startswith('http://') or url.startswith('https://') |
| |
| |
| def RequestGsAuthentication(): |
| """Requests that the user authenticate to be able to access gs:// as a |
| Googler. This allows much faster downloads, and pulling (old) toolchains |
| that match src/ revisions. |
| """ |
| print('Access to gs://chrome-wintoolchain/ not configured.') |
| print('-----------------------------------------------------------------') |
| print() |
| print('You appear to be a Googler.') |
| print() |
| print('I\'m sorry for the hassle, but you need to do a one-time manual') |
| print('authentication. Please run:') |
| print() |
| print(' download_from_google_storage --config') |
| print() |
| print('and follow the instructions.') |
| print() |
| print('NOTE 1: Use your google.com credentials, not chromium.org.') |
| print('NOTE 2: Enter 0 when asked for a "project-id".') |
| print() |
| print('-----------------------------------------------------------------') |
| print() |
| sys.stdout.flush() |
| sys.exit(1) |
| |
| |
| def DelayBeforeRemoving(target_dir): |
| """A grace period before deleting the out of date toolchain directory.""" |
| if (os.path.isdir(target_dir) and |
| not bool(int(os.environ.get('CHROME_HEADLESS', '0')))): |
| for i in range(9, 0, -1): |
| sys.stdout.write( |
| '\rRemoving old toolchain in %ds... (Ctrl-C to cancel)' % i) |
| sys.stdout.flush() |
| time.sleep(1) |
| print() |
| |
| |
| def DownloadUsingHttp(filename): |
| """Downloads the given file from a url defined in |
| DEPOT_TOOLS_WIN_TOOLCHAIN_BASE_URL environment variable.""" |
| import urlparse |
| import urllib2 |
| from contextlib import closing |
| temp_dir = tempfile.mkdtemp() |
| assert os.path.basename(filename) == filename |
| target_path = os.path.join(temp_dir, filename) |
| base_url = ToolchainBaseURL() |
| src_url = urlparse.urljoin(base_url, filename) |
| try: |
| with closing(urllib2.urlopen(src_url)) as fsrc, \ |
| open(target_path, 'wb') as fdst: |
| shutil.copyfileobj(fsrc, fdst) |
| except urllib2.URLError as e: |
| RmDir(temp_dir) |
| sys.exit('Failed to retrieve file: %s' % e) |
| return temp_dir, target_path |
| |
| |
| def DownloadUsingGsutil(filename): |
| """Downloads the given file from Google Storage chrome-wintoolchain bucket.""" |
| temp_dir = tempfile.mkdtemp() |
| assert os.path.basename(filename) == filename |
| target_path = os.path.join(temp_dir, filename) |
| gsutil = download_from_google_storage.Gsutil( |
| download_from_google_storage.GSUTIL_DEFAULT_PATH, boto_path=None) |
| code = gsutil.call('cp', 'gs://chrome-wintoolchain/' + filename, target_path) |
| if code != 0: |
| sys.exit('gsutil failed') |
| return temp_dir, target_path |
| |
| |
| def RmDir(path): |
| """Deletes path and all the files it contains.""" |
| if sys.platform != 'win32': |
| shutil.rmtree(path, ignore_errors=True) |
| else: |
| # shutil.rmtree() doesn't delete read-only files on Windows. |
| subprocess.check_call('rmdir /s/q "%s"' % path, shell=True) |
| |
| |
| def DoTreeMirror(target_dir, tree_sha1): |
| """In order to save temporary space on bots that do not have enough space to |
| download ISOs, unpack them, and copy to the target location, the whole tree |
| is uploaded as a zip to internal storage, and then mirrored here.""" |
| if UsesToolchainFromFile(): |
| temp_dir = None |
| local_zip = os.path.join(ToolchainBaseURL(), tree_sha1 + '.zip') |
| if not os.path.isfile(local_zip): |
| sys.exit('%s is not a valid file.' % local_zip) |
| elif UsesToolchainFromHttp(): |
| temp_dir, local_zip = DownloadUsingHttp(tree_sha1 + '.zip') |
| else: |
| temp_dir, local_zip = DownloadUsingGsutil(tree_sha1 + '.zip') |
| sys.stdout.write('Extracting %s...\n' % local_zip) |
| sys.stdout.flush() |
| with zipfile.ZipFile(local_zip, 'r', zipfile.ZIP_DEFLATED, True) as zf: |
| zf.extractall(target_dir) |
| if temp_dir: |
| RmDir(temp_dir) |
| |
| |
| def RemoveToolchain(root, sha1, delay_before_removing): |
| """Remove the |sha1| version of the toolchain from |root|.""" |
| toolchain_target_dir = os.path.join(root, sha1) |
| if delay_before_removing: |
| DelayBeforeRemoving(toolchain_target_dir) |
| if sys.platform == 'win32': |
| # These stay resident and will make the rmdir below fail. |
| kill_list = [ |
| 'mspdbsrv.exe', |
| 'vctip.exe', # Compiler and tools experience improvement data uploader. |
| ] |
| for process_name in kill_list: |
| with open(os.devnull, 'wb') as nul: |
| subprocess.call(['taskkill', '/f', '/im', process_name], |
| stdin=nul, stdout=nul, stderr=nul) |
| if os.path.isdir(toolchain_target_dir): |
| RmDir(toolchain_target_dir) |
| |
| timestamp_file = MakeTimestampsFileName(root, sha1) |
| if os.path.exists(timestamp_file): |
| os.remove(timestamp_file) |
| |
| |
| def RemoveUnusedToolchains(root): |
| """Remove the versions of the toolchain that haven't been used recently.""" |
| valid_toolchains = [] |
| dirs_to_remove = [] |
| |
| for d in os.listdir(root): |
| full_path = os.path.join(root, d) |
| if os.path.isdir(full_path): |
| if not os.path.exists(MakeTimestampsFileName(root, d)): |
| dirs_to_remove.append(d) |
| else: |
| vc_dir = os.path.join(full_path, 'VC') |
| valid_toolchains.append((os.path.getmtime(vc_dir), d)) |
| elif os.path.isfile(full_path): |
| os.remove(full_path) |
| |
| for d in dirs_to_remove: |
| print('Removing %s as it doesn\'t correspond to any known toolchain.' % |
| os.path.join(root, d)) |
| # Use the RemoveToolchain function to remove these directories as they might |
| # contain an older version of the toolchain. |
| RemoveToolchain(root, d, False) |
| |
| # Remove the versions of the toolchains that haven't been used in the past 30 |
| # days. |
| toolchain_expiration_time = 60 * 60 * 24 * 30 |
| for toolchain in valid_toolchains: |
| toolchain_age_in_sec = time.time() - toolchain[0] |
| if toolchain_age_in_sec > toolchain_expiration_time: |
| print('Removing version %s of the Win toolchain as it hasn\'t been used' |
| ' in the past %d days.' % (toolchain[1], |
| toolchain_age_in_sec / 60 / 60 / 24)) |
| RemoveToolchain(root, toolchain[1], True) |
| |
| |
| def EnableCrashDumpCollection(): |
| """Tell Windows Error Reporting to record crash dumps so that we can diagnose |
| linker crashes and other toolchain failures. Documented at: |
| https://msdn.microsoft.com/en-us/library/windows/desktop/bb787181.aspx |
| """ |
| if sys.platform == 'win32' and os.environ.get('CHROME_HEADLESS') == '1': |
| key_name = r'SOFTWARE\Microsoft\Windows\Windows Error Reporting' |
| try: |
| key = winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE, key_name, 0, |
| winreg.KEY_WOW64_64KEY | winreg.KEY_ALL_ACCESS) |
| # Merely creating LocalDumps is sufficient to enable the defaults. |
| winreg.CreateKey(key, "LocalDumps") |
| # Disable the WER UI, as documented here: |
| # https://msdn.microsoft.com/en-us/library/windows/desktop/bb513638.aspx |
| winreg.SetValueEx(key, "DontShowUI", 0, winreg.REG_DWORD, 1) |
| # Trap OSError instead of WindowsError so pylint will succeed on Linux. |
| # Catching errors is important because some build machines are not elevated |
| # and writing to HKLM requires elevation. |
| except OSError: |
| pass |
| |
| |
| def main(): |
| parser = optparse.OptionParser(description=sys.modules[__name__].__doc__) |
| parser.add_option('--output-json', metavar='FILE', |
| help='write information about toolchain to FILE') |
| parser.add_option('--force', action='store_true', |
| help='force script to run on non-Windows hosts') |
| parser.add_option('--no-download', action='store_true', |
| help='configure if present but don\'t download') |
| parser.add_option('--toolchain-dir', |
| default=os.getenv(ENV_TOOLCHAIN_ROOT, BASEDIR), |
| help='directory to install toolchain into') |
| options, args = parser.parse_args() |
| |
| if not (sys.platform.startswith(('cygwin', 'win32')) or options.force): |
| return 0 |
| |
| if sys.platform == 'cygwin': |
| # This script requires Windows Python, so invoke with depot_tools' Python. |
| def winpath(path): |
| return subprocess.check_output(['cygpath', '-w', path]).strip() |
| python = os.path.join(DEPOT_TOOLS_PATH, 'python.bat') |
| cmd = [python, winpath(__file__)] |
| if options.output_json: |
| cmd.extend(['--output-json', winpath(options.output_json)]) |
| cmd.extend(args) |
| sys.exit(subprocess.call(cmd)) |
| assert sys.platform != 'cygwin' |
| |
| if len(args) == 0: |
| sys.exit('Desired hash is required.') |
| desired_hash = args[0] |
| |
| # Create our toolchain destination and "chdir" to it. |
| toolchain_dir = os.path.abspath(options.toolchain_dir) |
| if not os.path.isdir(toolchain_dir): |
| os.makedirs(toolchain_dir) |
| os.chdir(toolchain_dir) |
| |
| # Move to depot_tools\win_toolchain where we'll store our files, and where |
| # the downloader script is. |
| if os.environ.get('GYP_MSVS_VERSION') == '2013': |
| target_dir = 'vs2013_files' |
| else: |
| target_dir = 'vs_files' |
| if not os.path.isdir(target_dir): |
| os.mkdir(target_dir) |
| toolchain_target_dir = os.path.join(target_dir, desired_hash) |
| |
| abs_toolchain_target_dir = os.path.abspath(toolchain_target_dir) |
| |
| got_new_toolchain = False |
| |
| # If the current hash doesn't match what we want in the file, nuke and pave. |
| # Typically this script is only run when the .sha1 one file is updated, but |
| # directly calling "gclient runhooks" will also run it, so we cache |
| # based on timestamps to make that case fast. |
| current_hashes = CalculateToolchainHashes(target_dir, True) |
| if desired_hash not in current_hashes: |
| if options.no_download: |
| raise SystemExit('Toolchain is out of date. Run "gclient runhooks" to ' |
| 'update the toolchain, or set ' |
| 'DEPOT_TOOLS_WIN_TOOLCHAIN=0 to use the locally ' |
| 'installed toolchain.') |
| should_use_file = False |
| should_use_http = False |
| should_use_gs = False |
| if UsesToolchainFromFile(): |
| should_use_file = True |
| elif UsesToolchainFromHttp(): |
| should_use_http = True |
| elif (HaveSrcInternalAccess() or |
| LooksLikeGoogler() or |
| CanAccessToolchainBucket()): |
| should_use_gs = True |
| if not CanAccessToolchainBucket(): |
| RequestGsAuthentication() |
| if not should_use_file and not should_use_gs and not should_use_http: |
| if sys.platform not in ('win32', 'cygwin'): |
| doc = 'https://chromium.googlesource.com/chromium/src/+/master/docs/' \ |
| 'win_cross.md' |
| print('\n\n\nPlease follow the instructions at %s\n\n' % doc) |
| else: |
| doc = 'https://chromium.googlesource.com/chromium/src/+/master/docs/' \ |
| 'windows_build_instructions.md' |
| print('\n\n\nNo downloadable toolchain found. In order to use your ' |
| 'locally installed version of Visual Studio to build Chrome ' |
| 'please set DEPOT_TOOLS_WIN_TOOLCHAIN=0.\n' |
| 'For details search for DEPOT_TOOLS_WIN_TOOLCHAIN in the ' |
| 'instructions at %s\n\n' % doc) |
| return 1 |
| print('Windows toolchain out of date or doesn\'t exist, updating (Pro)...') |
| print(' current_hashes: %s' % ', '.join(current_hashes)) |
| print(' desired_hash: %s' % desired_hash) |
| sys.stdout.flush() |
| |
| DoTreeMirror(toolchain_target_dir, desired_hash) |
| |
| got_new_toolchain = True |
| |
| win_sdk = os.path.join(abs_toolchain_target_dir, 'win_sdk') |
| try: |
| version_file = os.path.join(toolchain_target_dir, 'VS_VERSION') |
| vc_dir = os.path.join(toolchain_target_dir, 'VC') |
| with open(version_file, 'rb') as f: |
| vs_version = f.read().strip() |
| # Touch the VC directory so we can use its timestamp to know when this |
| # version of the toolchain has been used for the last time. |
| os.utime(vc_dir, None) |
| except IOError: |
| # Older toolchains didn't have the VS_VERSION file, and used 'win8sdk' |
| # instead of just 'win_sdk'. |
| vs_version = '2013' |
| win_sdk = os.path.join(abs_toolchain_target_dir, 'win8sdk') |
| |
| data = { |
| 'path': abs_toolchain_target_dir, |
| 'version': vs_version, |
| 'win_sdk': win_sdk, |
| # Added for backwards compatibility with old toolchain packages. |
| 'win8sdk': win_sdk, |
| 'wdk': os.path.join(abs_toolchain_target_dir, 'wdk'), |
| 'runtime_dirs': [ |
| os.path.join(abs_toolchain_target_dir, 'sys64'), |
| os.path.join(abs_toolchain_target_dir, 'sys32'), |
| os.path.join(abs_toolchain_target_dir, 'sysarm64'), |
| ], |
| } |
| with open(os.path.join(target_dir, '..', 'data.json'), 'w') as f: |
| json.dump(data, f) |
| |
| if got_new_toolchain: |
| current_hashes = CalculateToolchainHashes(target_dir, False) |
| if desired_hash not in current_hashes: |
| print( |
| 'Got wrong hash after pulling a new toolchain. ' |
| 'Wanted \'%s\', got one of \'%s\'.' % ( |
| desired_hash, ', '.join(current_hashes)), file=sys.stderr) |
| return 1 |
| SaveTimestampsAndHash(target_dir, desired_hash) |
| |
| if options.output_json: |
| shutil.copyfile(os.path.join(target_dir, '..', 'data.json'), |
| options.output_json) |
| |
| EnableCrashDumpCollection() |
| |
| RemoveUnusedToolchains(target_dir) |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |