| Index: win_toolchain/get_toolchain_if_necessary.py
 | 
| diff --git a/win_toolchain/get_toolchain_if_necessary.py b/win_toolchain/get_toolchain_if_necessary.py
 | 
| index 0b6e62d726b88f1628cc6e43973adc322423470c..010a0ad23c12c11fff14e8b77954c4575f26d730 100755
 | 
| --- a/win_toolchain/get_toolchain_if_necessary.py
 | 
| +++ b/win_toolchain/get_toolchain_if_necessary.py
 | 
| @@ -80,19 +80,26 @@ def GetFileList(root):
 | 
|    return sorted(file_list, key=lambda s: s.replace('/', '\\'))
 | 
|  
 | 
|  
 | 
| -def MakeTimestampsFileName(root):
 | 
| -  return os.path.join(root, '..', '.timestamps')
 | 
| +def MakeTimestampsFileName(root, sha1):
 | 
| +  return os.path.join(root, os.pardir, '%s.timestamps' % sha1)
 | 
|  
 | 
|  
 | 
| -def CalculateHash(root):
 | 
| +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."""
 | 
| -  file_list = GetFileList(root)
 | 
| +  contents of those files, and returns as a hex string.
 | 
|  
 | 
| -  # Check whether we previously saved timestamps in $root/../.timestamps. If
 | 
| -  # we didn't, or they don't match, then do the full calculation, otherwise
 | 
| +  |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)
 | 
| +  timestamps_file = MakeTimestampsFileName(root, expected_hash)
 | 
|    timestamps_data = {'files': [], 'sha1': ''}
 | 
|    if os.path.exists(timestamps_file):
 | 
|      with open(timestamps_file, 'rb') as f:
 | 
| @@ -103,9 +110,13 @@ def CalculateHash(root):
 | 
|          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 os.stat(disk).st_mtime != cached[1]:
 | 
| +      if disk != cached[0] or (
 | 
| +          disk != vc_dir and os.path.getmtime(disk) != cached[1]):
 | 
|          matches = False
 | 
|          break
 | 
|    if matches:
 | 
| @@ -113,21 +124,36 @@ def CalculateHash(root):
 | 
|  
 | 
|    digest = hashlib.sha1()
 | 
|    for path in file_list:
 | 
| -    digest.update(str(path).replace('/', '\\'))
 | 
| +    path_without_hash = str(path).replace('/', '\\')
 | 
| +    if expected_hash:
 | 
| +      path_without_hash = path_without_hash.replace(
 | 
| +          os.path.join(root, expected_hash), root)
 | 
| +    digest.update(path_without_hash)
 | 
|      with open(path, 'rb') as f:
 | 
|        digest.update(f.read())
 | 
|    return digest.hexdigest()
 | 
|  
 | 
|  
 | 
| +def CalculateToolchainHashes(root):
 | 
| +  """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:
 | 
| +    hashes.append(CalculateHash(root, d))
 | 
| +  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(root)
 | 
| +  file_list = GetFileList(os.path.join(root, sha1))
 | 
|    timestamps_data = {
 | 
| -    'files': [[f, os.stat(f).st_mtime] for f in file_list],
 | 
| +    'files': [[f, os.path.getmtime(f)] for f in file_list],
 | 
|      'sha1': sha1,
 | 
|    }
 | 
| -  with open(MakeTimestampsFileName(root), 'wb') as f:
 | 
| +  with open(MakeTimestampsFileName(root, sha1), 'wb') as f:
 | 
|      json.dump(timestamps_data, f)
 | 
|  
 | 
|  
 | 
| @@ -237,6 +263,64 @@ def DoTreeMirror(target_dir, tree_sha1):
 | 
|      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 has 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 GetInstallerName():
 | 
|    """Return the name of the Windows 10 Universal C Runtime installer for the
 | 
|    current platform, or None if installer is not needed or not applicable.
 | 
| @@ -329,10 +413,9 @@ def main():
 | 
|      sys.exit(subprocess.call(cmd))
 | 
|    assert sys.platform != 'cygwin'
 | 
|  
 | 
| -  # We assume that the Pro hash is the first one.
 | 
| -  desired_hashes = args
 | 
| -  if len(desired_hashes) == 0:
 | 
| -    sys.exit('Desired hashes are required.')
 | 
| +  if len(args) == 0:
 | 
| +    sys.exit('Desired hash is required.')
 | 
| +  desired_hash = args[0]
 | 
|  
 | 
|    # Move to depot_tools\win_toolchain where we'll store our files, and where
 | 
|    # the downloader script is.
 | 
| @@ -342,7 +425,11 @@ def main():
 | 
|      target_dir = os.path.normpath(os.path.join(toolchain_dir, 'vs_files'))
 | 
|    else:
 | 
|      target_dir = os.path.normpath(os.path.join(toolchain_dir, 'vs2013_files'))
 | 
| -  abs_target_dir = os.path.abspath(target_dir)
 | 
| +  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
 | 
|  
 | 
| @@ -350,8 +437,8 @@ def main():
 | 
|    # 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_hash = CalculateHash(target_dir)
 | 
| -  if current_hash not in desired_hashes:
 | 
| +  current_hashes = CalculateToolchainHashes(target_dir)
 | 
| +  if desired_hash not in current_hashes:
 | 
|      should_use_gs = False
 | 
|      if (HaveSrcInternalAccess() or
 | 
|          LooksLikeGoogler() or
 | 
| @@ -365,68 +452,62 @@ def main():
 | 
|              'build-instructions-windows\n\n')
 | 
|        return 1
 | 
|      print('Windows toolchain out of date or doesn\'t exist, updating (Pro)...')
 | 
| -    print('  current_hash: %s' % current_hash)
 | 
| -    print('  desired_hashes: %s' % ', '.join(desired_hashes))
 | 
| +    print('  current_hashes: %s' % ', '.join(current_hashes))
 | 
| +    print('  desired_hash: %s' % desired_hash)
 | 
|      sys.stdout.flush()
 | 
| -    DelayBeforeRemoving(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(target_dir):
 | 
| -      RmDir(target_dir)
 | 
| -
 | 
| -    DoTreeMirror(target_dir, desired_hashes[0])
 | 
| +
 | 
| +    DoTreeMirror(toolchain_target_dir, desired_hash)
 | 
|  
 | 
|      got_new_toolchain = True
 | 
|  
 | 
| -  win_sdk = os.path.join(abs_target_dir, 'win_sdk')
 | 
| +  win_sdk = os.path.join(abs_toolchain_target_dir, 'win_sdk')
 | 
|    try:
 | 
| -    with open(os.path.join(target_dir, 'VS_VERSION'), 'rb') as f:
 | 
| +    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_target_dir, 'win8sdk')
 | 
| +    win_sdk = os.path.join(abs_toolchain_target_dir, 'win8sdk')
 | 
|  
 | 
|    data = {
 | 
| -      'path': abs_target_dir,
 | 
| +      '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_target_dir, 'wdk'),
 | 
| +      'wdk': os.path.join(abs_toolchain_target_dir, 'wdk'),
 | 
|        'runtime_dirs': [
 | 
| -        os.path.join(abs_target_dir, 'sys64'),
 | 
| -        os.path.join(abs_target_dir, 'sys32'),
 | 
| +        os.path.join(abs_toolchain_target_dir, 'sys64'),
 | 
| +        os.path.join(abs_toolchain_target_dir, 'sys32'),
 | 
|        ],
 | 
|    }
 | 
|    with open(os.path.join(target_dir, '..', 'data.json'), 'w') as f:
 | 
|      json.dump(data, f)
 | 
|  
 | 
|    if got_new_toolchain:
 | 
| -    current_hash = CalculateHash(target_dir)
 | 
| -    if current_hash not in desired_hashes:
 | 
| +    current_hashes = CalculateToolchainHashes(target_dir)
 | 
| +    if desired_hash not in current_hashes:
 | 
|        print >> sys.stderr, (
 | 
|            'Got wrong hash after pulling a new toolchain. '
 | 
| -          'Wanted one of \'%s\', got \'%s\'.' % (
 | 
| -              ', '.join(desired_hashes), current_hash))
 | 
| +          'Wanted \'%s\', got one of \'%s\'.' % (
 | 
| +              desired_hash, ', '.join(current_hashes)))
 | 
|        return 1
 | 
| -    SaveTimestampsAndHash(target_dir, current_hash)
 | 
| +    SaveTimestampsAndHash(target_dir, desired_hash)
 | 
|  
 | 
|    if options.output_json:
 | 
|      shutil.copyfile(os.path.join(target_dir, '..', 'data.json'),
 | 
|                      options.output_json)
 | 
|  
 | 
|    if os.environ.get('GYP_MSVS_VERSION') == '2015':
 | 
| -    InstallUniversalCRTIfNeeded(abs_target_dir)
 | 
| +    InstallUniversalCRTIfNeeded(abs_toolchain_target_dir)
 | 
| +
 | 
| +  RemoveUnusedToolchains(target_dir)
 | 
|  
 | 
|    return 0
 | 
|  
 | 
| 
 |