[infra] Update windows build and toolchain setup python scripts.

The scripts originate from Chromium, this CL syncs Dart's copy with Chromium tip of the tree at 6f8f710079b3e363f4fd7ffe3d848384e4b7c816.

Format toolchain/win/BUILD.gn

Change-Id: Ice7ba48bdd102ffe0e25c6ae6068f83cb14169ba
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/253500
Reviewed-by: Alexander Thomas <athom@google.com>
Commit-Queue: Alexander Aprelev <aam@google.com>
diff --git a/build/find_depot_tools.py b/build/find_depot_tools.py
new file mode 100644
index 0000000..903fc25
--- /dev/null
+++ b/build/find_depot_tools.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+# Copyright (c) 2011 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.
+#
+# For Dart/Flutter developers:
+# This file is copied from Chromium:
+#   https://cs.chromium.org/chromium/src/build/find_depot_tools.py
+# When updating replace reference to python on the first line with python3.
+#
+"""Small utility function to find depot_tools and add it to the python path.
+
+Will throw an ImportError exception if depot_tools can't be found since it
+imports breakpad.
+
+This can also be used as a standalone script to print out the depot_tools
+directory location.
+"""
+
+from __future__ import print_function
+
+import os
+import sys
+
+
+# Path to //src
+SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
+
+
+def IsRealDepotTools(path):
+  expanded_path = os.path.expanduser(path)
+  return os.path.isfile(os.path.join(expanded_path, 'gclient.py'))
+
+
+def add_depot_tools_to_path():
+  """Search for depot_tools and add it to sys.path."""
+  # First, check if we have a DEPS'd in "depot_tools".
+  deps_depot_tools = os.path.join(SRC, 'third_party', 'depot_tools')
+  if IsRealDepotTools(deps_depot_tools):
+    # Put the pinned version at the start of the sys.path, in case there
+    # are other non-pinned versions already on the sys.path.
+    sys.path.insert(0, deps_depot_tools)
+    return deps_depot_tools
+
+  # Then look if depot_tools is already in PYTHONPATH.
+  for i in sys.path:
+    if i.rstrip(os.sep).endswith('depot_tools') and IsRealDepotTools(i):
+      return i
+  # Then look if depot_tools is in PATH, common case.
+  for i in os.environ['PATH'].split(os.pathsep):
+    if IsRealDepotTools(i):
+      sys.path.append(i.rstrip(os.sep))
+      return i
+  # Rare case, it's not even in PATH, look upward up to root.
+  root_dir = os.path.dirname(os.path.abspath(__file__))
+  previous_dir = os.path.abspath(__file__)
+  while root_dir and root_dir != previous_dir:
+    i = os.path.join(root_dir, 'depot_tools')
+    if IsRealDepotTools(i):
+      sys.path.append(i)
+      return i
+    previous_dir = root_dir
+    root_dir = os.path.dirname(root_dir)
+  print('Failed to find depot_tools', file=sys.stderr)
+  return None
+
+DEPOT_TOOLS_PATH = add_depot_tools_to_path()
+
+# pylint: disable=W0611
+import breakpad
+
+
+def main():
+  if DEPOT_TOOLS_PATH is None:
+    return 1
+  print(DEPOT_TOOLS_PATH)
+  return 0
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/build/toolchain/win/BUILD.gn b/build/toolchain/win/BUILD.gn
index d70d49e..bc54953 100644
--- a/build/toolchain/win/BUILD.gn
+++ b/build/toolchain/win/BUILD.gn
@@ -146,9 +146,10 @@
     }
 
     tool("solink") {
-      dllname = "{{root_out_dir}}/{{target_output_name}}{{output_extension}}"  # e.g. foo.dll
-      libname =
-          "{{root_out_dir}}/{{target_output_name}}{{output_extension}}.lib"  # e.g. foo.dll.lib
+      dllname = "{{root_out_dir}}/{{target_output_name}}{{output_extension}}"  # e.g.
+                                                                               # foo.dll
+      libname = "{{root_out_dir}}/{{target_output_name}}{{output_extension}}.lib"  # e.g.
+                                                                                   # foo.dll.lib
       rspfile = "${dllname}.rsp"
 
       link_command = "$python_path $tool_wrapper_path link-wrapper $env False link.exe /nologo /IMPLIB:$libname /DLL /OUT:$dllname /PDB:${dllname}.pdb @$rspfile"
@@ -175,7 +176,9 @@
     }
 
     tool("solink_module") {
-      dllname = "{{output_dir}}/{{target_output_name}}{{output_extension}}"  # e.g. foo.dll
+      dllname =
+          "{{output_dir}}/{{target_output_name}}{{output_extension}}"  # e.g.
+                                                                       # foo.dll
       pdbname = "${dllname}.pdb"
       rspfile = "${dllname}.rsp"
 
@@ -230,8 +233,7 @@
     }
 
     tool("copy") {
-      command =
-          "$python_path $tool_wrapper_path recursive-mirror {{source}} {{output}}"
+      command = "$python_path $tool_wrapper_path recursive-mirror {{source}} {{output}}"
       description = "COPY {{source}} {{output}}"
     }
   }
@@ -246,7 +248,9 @@
                                      visual_studio_path,
                                      windows_sdk_path,
                                      visual_studio_runtime_dirs,
+                                     "win",
                                      toolchain_arch,
+                                     "environment." + toolchain_arch,
                                    ],
                                    "scope")
 
diff --git a/build/toolchain/win/setup_toolchain.py b/build/toolchain/win/setup_toolchain.py
index 10b878a..e680ba0 100644
--- a/build/toolchain/win/setup_toolchain.py
+++ b/build/toolchain/win/setup_toolchain.py
@@ -10,63 +10,183 @@
 # win tool. The script assumes that the root build directory is the current dir
 # and the files will be written to the current directory.
 
+from __future__ import print_function
+
 import errno
+import json
 import os
 import re
 import subprocess
 import sys
 
+sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
+import gn_helpers
+
+SCRIPT_DIR = os.path.dirname(__file__)
 
 def _ExtractImportantEnvironment(output_of_set):
   """Extracts environment variables required for the toolchain to run from
   a textual dump output by the cmd.exe 'set' command."""
   envvars_to_save = (
+      'cipd_cache_dir', # needed by vpython
+      'homedrive', # needed by vpython
+      'homepath', # needed by vpython
       'goma_.*', # TODO(scottmg): This is ugly, but needed for goma.
       'include',
       'lib',
       'libpath',
+      'luci_context', # needed by vpython
       'path',
       'pathext',
       'systemroot',
       'temp',
       'tmp',
+      'userprofile', # needed by vpython
+      'vpython_virtualenv_root' # needed by vpython
       )
   env = {}
+  # This occasionally happens and leads to misleading SYSTEMROOT error messages
+  # if not caught here.
+  if output_of_set.count('=') == 0:
+    raise Exception('Invalid output_of_set. Value is:\n%s' % output_of_set)
   for line in output_of_set.splitlines():
     for envvar in envvars_to_save:
-      if re.match(envvar + '=', line.decode().lower()):
-        var, setting = line.decode().split('=', 1)
+      if re.match(envvar + '=', line.lower()):
+        var, setting = line.split('=', 1)
         if envvar == 'path':
-          # Our own rules (for running gyp-win-tool) and other actions in
-          # Chromium rely on python being in the path. Add the path to this
-          # python here so that if it's not in the path when ninja is run
-          # later, python will still be found.
+          # Our own rules and actions in Chromium rely on python being in the
+          # path. Add the path to this python here so that if it's not in the
+          # path when ninja is run later, python will still be found.
           setting = os.path.dirname(sys.executable) + os.pathsep + setting
+        if envvar in ['include', 'lib']:
+          # Make sure that the include and lib paths point to directories that
+          # exist. This ensures a (relatively) clear error message if the
+          # required SDK is not installed.
+          for part in setting.split(';'):
+            if not os.path.exists(part) and len(part) != 0:
+              raise Exception(
+                  'Path "%s" from environment variable "%s" does not exist. '
+                  'Make sure the necessary SDK is installed.' % (part, envvar))
         env[var.upper()] = setting
         break
-  for required in ('SYSTEMROOT', 'TEMP', 'TMP'):
-    if required not in env:
-      raise Exception('Environment variable "%s" '
-                      'required to be set to valid path' % required)
+  if sys.platform in ('win32', 'cygwin'):
+    for required in ('SYSTEMROOT', 'TEMP', 'TMP'):
+      if required not in env:
+        raise Exception('Environment variable "%s" '
+                        'required to be set to valid path' % required)
   return env
 
 
-def _SetupScript(target_cpu, sdk_dir):
-  """Returns a command (with arguments) to be used to set up the
-  environment."""
+def _DetectVisualStudioPath():
+  """Return path to the installed Visual Studio.
+  """
+
+  # Use the code in build/vs_toolchain.py to avoid duplicating code.
+  chromium_dir = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..', '..'))
+  sys.path.append(os.path.join(chromium_dir, 'build'))
+  import vs_toolchain
+  return vs_toolchain.DetectVisualStudioPath()
+
+
+def _LoadEnvFromBat(args):
+  """Given a bat command, runs it and returns env vars set by it."""
+  args = args[:]
+  args.extend(('&&', 'set'))
+  popen = subprocess.Popen(
+      args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+  variables, _ = popen.communicate()
+  if popen.returncode != 0:
+    raise Exception('"%s" failed with error %d' % (args, popen.returncode))
+  return variables.decode(errors='ignore')
+
+
+def _LoadToolchainEnv(cpu, toolchain_root, sdk_dir, target_store):
+  """Returns a dictionary with environment variables that must be set while
+  running binaries from the toolchain (e.g. INCLUDE and PATH for cl.exe)."""
   # Check if we are running in the SDK command line environment and use
-  # the setup script from the SDK if so.
-  assert target_cpu in ('x86', 'x64', 'arm64')
+  # the setup script from the SDK if so. |cpu| should be either
+  # 'x86' or 'x64' or 'arm' or 'arm64'.
+  assert cpu in ('x86', 'x64', 'arm', 'arm64')
   if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and sdk_dir:
-    return [os.path.normpath(os.path.join(sdk_dir, 'Bin/SetEnv.Cmd')),
-            '/' + target_cpu]
+    # Load environment from json file.
+    env = os.path.normpath(os.path.join(sdk_dir, 'bin/SetEnv.%s.json' % cpu))
+    env = json.load(open(env))['env']
+    if env['VSINSTALLDIR'] == [["..", "..\\"]]:
+      # Old-style paths were relative to the win_sdk\bin directory.
+      json_relative_dir = os.path.join(sdk_dir, 'bin')
+    else:
+      # New-style paths are relative to the toolchain directory.
+      json_relative_dir = toolchain_root
+    for k in env:
+      entries = [os.path.join(*([json_relative_dir] + e)) for e in env[k]]
+      # clang-cl wants INCLUDE to be ;-separated even on non-Windows,
+      # lld-link wants LIB to be ;-separated even on non-Windows.  Path gets :.
+      # The separator for INCLUDE here must match the one used in main() below.
+      sep = os.pathsep if k == 'PATH' else ';'
+      env[k] = sep.join(entries)
+    # PATH is a bit of a special case, it's in addition to the current PATH.
+    env['PATH'] = env['PATH'] + os.pathsep + os.environ['PATH']
+    # Augment with the current env to pick up TEMP and friends.
+    for k in os.environ:
+      if k not in env:
+        env[k] = os.environ[k]
+
+    varlines = []
+    for k in sorted(env.keys()):
+      varlines.append('%s=%s' % (str(k), str(env[k])))
+    variables = '\n'.join(varlines)
+
+    # Check that the json file contained the same environment as the .cmd file.
+    if sys.platform in ('win32', 'cygwin'):
+      script = os.path.normpath(os.path.join(sdk_dir, 'Bin/SetEnv.cmd'))
+      arg = '/' + cpu
+      json_env = _ExtractImportantEnvironment(variables)
+      cmd_env = _ExtractImportantEnvironment(_LoadEnvFromBat([script, arg]))
+      assert _LowercaseDict(json_env) == _LowercaseDict(cmd_env)
   else:
+    if 'GYP_MSVS_OVERRIDE_PATH' not in os.environ:
+      os.environ['GYP_MSVS_OVERRIDE_PATH'] = _DetectVisualStudioPath()
     # We only support x64-hosted tools.
-    # TODO(scottmg|dpranke): Non-depot_tools toolchain: need to get Visual
-    # Studio install location from registry.
-    return [os.path.normpath(os.path.join(os.environ['GYP_MSVS_OVERRIDE_PATH'],
-                                          'VC/Auxiliary/Build/vcvarsall.bat')),
-            'amd64_x86' if target_cpu == 'x86' else 'amd64']
+    script_path = os.path.normpath(os.path.join(
+                                       os.environ['GYP_MSVS_OVERRIDE_PATH'],
+                                       'VC/vcvarsall.bat'))
+    if not os.path.exists(script_path):
+      # vcvarsall.bat for VS 2017 fails if run after running vcvarsall.bat from
+      # VS 2013 or VS 2015. Fix this by clearing the vsinstalldir environment
+      # variable. Since vcvarsall.bat appends to the INCLUDE, LIB, and LIBPATH
+      # environment variables we need to clear those to avoid getting double
+      # entries when vcvarsall.bat has been run before gn gen. vcvarsall.bat
+      # also adds to PATH, but there is no clean way of clearing that and it
+      # doesn't seem to cause problems.
+      if 'VSINSTALLDIR' in os.environ:
+        del os.environ['VSINSTALLDIR']
+        if 'INCLUDE' in os.environ:
+          del os.environ['INCLUDE']
+        if 'LIB' in os.environ:
+          del os.environ['LIB']
+        if 'LIBPATH' in os.environ:
+          del os.environ['LIBPATH']
+      other_path = os.path.normpath(os.path.join(
+                                        os.environ['GYP_MSVS_OVERRIDE_PATH'],
+                                        'VC/Auxiliary/Build/vcvarsall.bat'))
+      if not os.path.exists(other_path):
+        raise Exception('%s is missing - make sure VC++ tools are installed.' %
+                        script_path)
+      script_path = other_path
+    cpu_arg = "amd64"
+    if (cpu != 'x64'):
+      # x64 is default target CPU thus any other CPU requires a target set
+      cpu_arg += '_' + cpu
+    args = [script_path, cpu_arg, ]
+    # Store target must come before any SDK version declaration
+    if (target_store):
+      args.append('store')
+    # Explicitly specifying the SDK version to build with to avoid accidentally
+    # building with a new and untested SDK. This should stay in sync with the
+    # packaged toolchain in build/vs_toolchain.py.
+    args.append('10.0.20348.0')
+    variables = _LoadEnvFromBat(args)
+  return _ExtractImportantEnvironment(variables)
 
 
 def _FormatAsEnvironmentBlock(envvar_dict):
@@ -81,76 +201,114 @@
   return block
 
 
-def _CopyTool(source_path):
-  """Copies the given tool to the current directory, including a warning not
-  to edit it."""
-  with open(source_path) as source_file:
-    tool_source = source_file.readlines()
+def _LowercaseDict(d):
+  """Returns a copy of `d` with both key and values lowercased.
 
-  # Add header and write it out to the current directory (which should be the
-  # root build dir).
-  with open("gyp-win-tool", 'w') as tool_file:
-    tool_file.write(''.join([tool_source[0],
-                             '# Generated by setup_toolchain.py do not edit.\n']
-                            + tool_source[1:]))
+  Args:
+    d: dict to lowercase (e.g. {'A': 'BcD'}).
+
+  Returns:
+    A dict with both keys and values lowercased (e.g.: {'a': 'bcd'}).
+  """
+  return {k.lower(): d[k].lower() for k in d}
+
+
+def FindFileInEnvList(env, env_name, separator, file_name, optional=False):
+  parts = env[env_name].split(separator)
+  for path in parts:
+    if os.path.exists(os.path.join(path, file_name)):
+      return os.path.realpath(path)
+  assert optional, "%s is not found in %s:\n%s\nCheck if it is installed." % (
+      file_name, env_name, '\n'.join(parts))
+  return ''
 
 
 def main():
-  if len(sys.argv) != 5:
+  if len(sys.argv) != 7:
     print('Usage setup_toolchain.py '
           '<visual studio path> <win sdk path> '
-          '<runtime dirs> <target_cpu>')
+          '<runtime dirs> <target_os> <target_cpu> '
+          '<environment block name|none>')
     sys.exit(2)
+  # toolchain_root and win_sdk_path are only read if the hermetic Windows
+  # toolchain is set, that is if DEPOT_TOOLS_WIN_TOOLCHAIN is not set to 0.
+  # With the hermetic Windows toolchain, the visual studio path in argv[1]
+  # is the root of the Windows toolchain directory.
+  toolchain_root = sys.argv[1]
   win_sdk_path = sys.argv[2]
-  runtime_dirs = sys.argv[3]
-  target_cpu = sys.argv[4]
 
-  cpus = ('x86', 'x64', 'arm64')
+  runtime_dirs = sys.argv[3]
+  target_os = sys.argv[4]
+  target_cpu = sys.argv[5]
+  environment_block_name = sys.argv[6]
+  if (environment_block_name == 'none'):
+    environment_block_name = ''
+
+  if (target_os == 'winuwp'):
+    target_store = True
+  else:
+    target_store = False
+
+  cpus = ('x86', 'x64', 'arm', 'arm64')
   assert target_cpu in cpus
   vc_bin_dir = ''
+  include = ''
+  lib = ''
 
   # TODO(scottmg|goma): Do we need an equivalent of
   # ninja_use_custom_environment_files?
 
+  def relflag(s):  # Make s relative to builddir when cwd and sdk on same drive.
+    try:
+      return os.path.relpath(s).replace('\\', '/')
+    except ValueError:
+      return s
+
+  def q(s):  # Quote s if it contains spaces or other weird characters.
+    return s if re.match(r'^[a-zA-Z0-9._/\\:-]*$', s) else '"' + s + '"'
+
   for cpu in cpus:
-    # Extract environment variables for subprocesses.
-    args = _SetupScript(cpu, win_sdk_path)
-    args.extend(('&&', 'set'))
-    popen = subprocess.Popen(
-        args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-    stdout_data, stderr_data = popen.communicate()
-    if popen.returncode != 0:
-      print('Error, got returncode:', popen.returncode)
-      print('## stdout:')
-      print(stdout_data)
-      print('## stderr:')
-      print(stderr_data)
-      sys.exit(2)
-    env = _ExtractImportantEnvironment(stdout_data)
-    env['PATH'] = runtime_dirs + ';' + env['PATH']
-
     if cpu == target_cpu:
-      for path in env['PATH'].split(os.pathsep):
-        if os.path.exists(os.path.join(path, 'cl.exe')):
-          vc_bin_dir = os.path.realpath(path)
-          break
+      # Extract environment variables for subprocesses.
+      env = _LoadToolchainEnv(cpu, toolchain_root, win_sdk_path, target_store)
+      env['PATH'] = runtime_dirs + os.pathsep + env['PATH']
 
-    # The Windows SDK include directories must be first. They both have a sal.h,
-    # and the SDK one is newer and the SDK uses some newer features from it not
-    # present in the Visual Studio one.
+      vc_bin_dir = FindFileInEnvList(env, 'PATH', os.pathsep, 'cl.exe')
 
-    if win_sdk_path:
-      additional_includes = ('{sdk_dir}\\Include\\shared;' +
-                             '{sdk_dir}\\Include\\um;' +
-                             '{sdk_dir}\\Include\\winrt;').format(
-                                  sdk_dir=win_sdk_path)
-      env['INCLUDE'] = additional_includes + env['INCLUDE']
-    env_block = _FormatAsEnvironmentBlock(env)
-    with open('environment.' + cpu, 'wb') as f:
-      f.write(env_block.encode())
+      # The separator for INCLUDE here must match the one used in
+      # _LoadToolchainEnv() above.
+      include = [p.replace('"', r'\"') for p in env['INCLUDE'].split(';') if p]
+      include = list(map(relflag, include))
 
-  assert vc_bin_dir
-  print('vc_bin_dir = "%s"' % vc_bin_dir)
+      lib = [p.replace('"', r'\"') for p in env['LIB'].split(';') if p]
+      lib = list(map(relflag, lib))
+
+      include_I = ' '.join([q('/I' + i) for i in include])
+      include_imsvc = ' '.join([q('-imsvc' + i) for i in include])
+      libpath_flags = ' '.join([q('-libpath:' + i) for i in lib])
+
+      if (environment_block_name != ''):
+        env_block = _FormatAsEnvironmentBlock(env)
+        with open(environment_block_name, 'w') as f:
+          f.write(env_block)
+
+  print('vc_bin_dir = ' + gn_helpers.ToGNString(vc_bin_dir))
+  assert include_I
+  print('include_flags_I = ' + gn_helpers.ToGNString(include_I))
+  assert include_imsvc
+  if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and win_sdk_path:
+    print('include_flags_imsvc = ' +
+          gn_helpers.ToGNString(q('/winsysroot' + relflag(toolchain_root))))
+  else:
+    print('include_flags_imsvc = ' + gn_helpers.ToGNString(include_imsvc))
+  print('paths = ' + gn_helpers.ToGNString(env['PATH']))
+  assert libpath_flags
+  print('libpath_flags = ' + gn_helpers.ToGNString(libpath_flags))
+  if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and win_sdk_path:
+    print('libpath_lldlink_flags = ' +
+          gn_helpers.ToGNString(q('/winsysroot:' + relflag(toolchain_root))))
+  else:
+    print('libpath_lldlink_flags = ' + gn_helpers.ToGNString(libpath_flags))
 
 
 if __name__ == '__main__':
diff --git a/build/vs_toolchain.py b/build/vs_toolchain.py
index 6314e5e..9b0a7c8 100644
--- a/build/vs_toolchain.py
+++ b/build/vs_toolchain.py
@@ -1,18 +1,14 @@
 #!/usr/bin/env python3
-#
 # Copyright 2014 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.
-
+#
 # For Dart/Flutter developers:
-# This file keeps the MSVC toolchain up-to-date for Google developers.
-# It is copied from Chromium:
+# This file is copied from Chromium:
 #   https://cs.chromium.org/chromium/src/build/vs_toolchain.py
-# with modifications that update paths, and remove dependencies on gyp.
-# To update to a new MSVC toolchain, copy the updated script from the Chromium
-# tree, and edit to make it work in the Dart tree by updating paths in the original script.
-
-
+# When updating replace reference to python on the first line with python3.
+#
+from __future__ import print_function
 
 import collections
 import glob
@@ -26,21 +22,47 @@
 import subprocess
 import sys
 
-
 from gn_helpers import ToGNString
 
+# VS 2019 16.61 with 10.0.20348.0 SDK, 10.0.19041 version of Debuggers
+# with ARM64 libraries and UWP support.
+# See go/chromium-msvc-toolchain for instructions about how to update the
+# toolchain.
+#
+# When updating the toolchain, consider the following areas impacted by the
+# toolchain version:
+#
+# * //base/win/windows_version.cc NTDDI preprocessor check
+#   Triggers a compiler error if the available SDK is older than the minimum.
+# * //build/config/win/BUILD.gn NTDDI_VERSION value
+#   Affects the availability of APIs in the toolchain headers.
+# * //docs/windows_build_instructions.md mentions of VS or Windows SDK.
+#   Keeps the document consistent with the toolchain version.
+TOOLCHAIN_HASH = '1023ce2e82'
+
 script_dir = os.path.dirname(os.path.realpath(__file__))
 json_data_file = os.path.join(script_dir, 'win_toolchain.json')
 
-sys.path.insert(0, os.path.join(script_dir, '..', 'tools'))
-
 # VS versions are listed in descending order of priority (highest first).
+# The first version is assumed by this script to be the one that is packaged,
+# which makes a difference for the arm64 runtime.
 MSVS_VERSIONS = collections.OrderedDict([
-  ('2017', '15.0'),
-  ('2019', '16.0'),
-  ('2022', '17.0'),
+    ('2019', '16.0'),  # Default and packaged version of Visual Studio.
+    ('2022', '17.0'),
+    ('2017', '15.0'),
 ])
 
+# List of preferred VC toolset version based on MSVS
+# Order is not relevant for this dictionary.
+MSVC_TOOLSET_VERSION = {
+    '2022': 'VC143',
+    '2019': 'VC142',
+    '2017': 'VC141',
+}
+
+def _HostIsWindows():
+  """Returns True if running on a Windows host (including under cygwin)."""
+  return sys.platform in ('win32', 'cygwin')
 
 def SetEnvironmentAndGetRuntimeDllDirs():
   """Sets up os.environ to use the depot_tools VS toolchain with gyp, and
@@ -56,7 +78,7 @@
       bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1')))
   # When running on a non-Windows host, only do this if the SDK has explicitly
   # been downloaded before (in which case json_data_file will exist).
-  if ((sys.platform in ('win32', 'cygwin') or os.path.exists(json_data_file))
+  if ((_HostIsWindows() or os.path.exists(json_data_file))
       and depot_tools_win_toolchain):
     if ShouldUpdateToolchain():
       if len(sys.argv) > 1 and sys.argv[1] == 'update':
@@ -71,8 +93,6 @@
     toolchain = toolchain_data['path']
     version = toolchain_data['version']
     win_sdk = toolchain_data.get('win_sdk')
-    if not win_sdk:
-      win_sdk = toolchain_data['win8sdk']
     wdk = toolchain_data['wdk']
     # TODO(scottmg): The order unfortunately matters in these. They should be
     # split into separate keys for x64/x86/arm64. (See CopyDlls call below).
@@ -85,7 +105,6 @@
       vs_runtime_dll_dirs.append('Arm64Unused')
 
     os.environ['GYP_MSVS_OVERRIDE_PATH'] = toolchain
-    os.environ['GYP_MSVS_VERSION'] = version
 
     os.environ['WINDOWSSDKDIR'] = win_sdk
     os.environ['WDK_DIR'] = wdk
@@ -95,8 +114,6 @@
   elif sys.platform == 'win32' and not depot_tools_win_toolchain:
     if not 'GYP_MSVS_OVERRIDE_PATH' in os.environ:
       os.environ['GYP_MSVS_OVERRIDE_PATH'] = DetectVisualStudioPath()
-    if not 'GYP_MSVS_VERSION' in os.environ:
-      os.environ['GYP_MSVS_VERSION'] = GetVisualStudioVersion()
 
     # When using an installed toolchain these files aren't needed in the output
     # directory in order to run binaries locally, but they are needed in order
@@ -126,12 +143,12 @@
     contents of the registry key's value, or None on failure.  Throws
     ImportError if _winreg is unavailable.
   """
-  import winreg
+  import _winreg
   try:
     root, subkey = key.split('\\', 1)
     assert root == 'HKLM'  # Only need HKLM for now.
-    with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, subkey) as hkey:
-      return winreg.QueryValueEx(hkey, value)[0]
+    with _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, subkey) as hkey:
+      return _winreg.QueryValueEx(hkey, value)[0]
   except WindowsError:
     return None
 
@@ -146,31 +163,38 @@
 def GetVisualStudioVersion():
   """Return best available version of Visual Studio.
   """
-
-  env_version = os.environ.get('GYP_MSVS_VERSION')
-  if env_version:
-    return env_version
-
   supported_versions = list(MSVS_VERSIONS.keys())
 
   # VS installed in depot_tools for Googlers
   if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1'))):
-    return list(supported_versions)[0]
+    return supported_versions[0]
 
   # VS installed in system for external developers
   supported_versions_str = ', '.join('{} ({})'.format(v,k)
-      for k,v in list(MSVS_VERSIONS.items()))
+      for k,v in MSVS_VERSIONS.items())
   available_versions = []
   for version in supported_versions:
-    for path in (
-        os.environ.get('vs%s_install' % version),
-        os.path.expandvars('%ProgramFiles(x86)%' +
-                           '/Microsoft Visual Studio/%s' % version),
-        os.path.expandvars('%ProgramFiles%' +
-                           '/Microsoft Visual Studio/%s' % version)):
-      if path and os.path.exists(path):
-        available_versions.append(version)
-        break
+    # Checking vs%s_install environment variables.
+    # For example, vs2019_install could have the value
+    # "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community".
+    # Only vs2017_install, vs2019_install and vs2022_install are supported.
+    path = os.environ.get('vs%s_install' % version)
+    if path and os.path.exists(path):
+      available_versions.append(version)
+      break
+    # Detecting VS under possible paths.
+    if version >= '2022':
+      program_files_path_variable = '%ProgramFiles%'
+    else:
+      program_files_path_variable = '%ProgramFiles(x86)%'
+    path = os.path.expandvars(program_files_path_variable +
+                              '/Microsoft Visual Studio/%s' % version)
+    if path and any(
+        os.path.exists(os.path.join(path, edition))
+        for edition in ('Enterprise', 'Professional', 'Community', 'Preview',
+                        'BuildTools')):
+      available_versions.append(version)
+      break
 
   if not available_versions:
     raise Exception('No supported Visual Studio can be found.'
@@ -179,7 +203,7 @@
 
 
 def DetectVisualStudioPath():
-  """Return path to the GYP_MSVS_VERSION of Visual Studio.
+  """Return path to the installed Visual Studio.
   """
 
   # Note that this code is used from
@@ -190,18 +214,30 @@
   # the registry. For details see:
   # https://blogs.msdn.microsoft.com/heaths/2016/09/15/changes-to-visual-studio-15-setup/
   # For now we use a hardcoded default with an environment variable override.
-  possible_install_paths = (
-      os.path.expandvars('%s/Microsoft Visual Studio/%s/%s' %
-                         (program_path_var, version_as_year, product))
-      for program_path_var in ('%ProgramFiles%', '%ProgramFiles(x86)%')
-      for product in ('Enterprise', 'Professional', 'Community', 'Preview'))
-  for path in (
-      os.environ.get('vs%s_install' % version_as_year), *possible_install_paths):
+  if version_as_year >= '2022':
+    program_files_path_variable = '%ProgramFiles%'
+  else:
+    program_files_path_variable = '%ProgramFiles(x86)%'
+  for path in (os.environ.get('vs%s_install' % version_as_year),
+               os.path.expandvars(program_files_path_variable +
+                                  '/Microsoft Visual Studio/%s/Enterprise' %
+                                  version_as_year),
+               os.path.expandvars(program_files_path_variable +
+                                  '/Microsoft Visual Studio/%s/Professional' %
+                                  version_as_year),
+               os.path.expandvars(program_files_path_variable +
+                                  '/Microsoft Visual Studio/%s/Community' %
+                                  version_as_year),
+               os.path.expandvars(program_files_path_variable +
+                                  '/Microsoft Visual Studio/%s/Preview' %
+                                  version_as_year),
+               os.path.expandvars(program_files_path_variable +
+                                  '/Microsoft Visual Studio/%s/BuildTools' %
+                                  version_as_year)):
     if path and os.path.exists(path):
       return path
 
-  raise Exception('Visual Studio Version %s (from GYP_MSVS_VERSION)'
-                  ' not found.' % version_as_year)
+  raise Exception('Visual Studio Version %s not found.' % version_as_year)
 
 
 def _CopyRuntimeImpl(target, source, verbose=True):
@@ -243,21 +279,30 @@
 
   list_of_str_versions.sort(key=to_number_sequence, reverse=True)
 
-def _CopyUCRTRuntime(target_dir, source_dir, target_cpu, dll_pattern, suffix):
+
+def _CopyUCRTRuntime(target_dir, source_dir, target_cpu, suffix):
   """Copy both the msvcp and vccorlib runtime DLLs, only if the target doesn't
   exist, but the target directory does exist."""
   if target_cpu == 'arm64':
     # Windows ARM64 VCRuntime is located at {toolchain_root}/VC/Redist/MSVC/
-    # {x.y.z}/[debug_nonredist/]arm64/Microsoft.VC141.CRT/.
+    # {x.y.z}/[debug_nonredist/]arm64/Microsoft.VC14x.CRT/.
+    # Select VC toolset directory based on Visual Studio version
     vc_redist_root = FindVCRedistRoot()
     if suffix.startswith('.'):
+      vc_toolset_dir = 'Microsoft.{}.CRT' \
+         .format(MSVC_TOOLSET_VERSION[GetVisualStudioVersion()])
       source_dir = os.path.join(vc_redist_root,
-                                'arm64', 'Microsoft.VC141.CRT')
+                                'arm64', vc_toolset_dir)
     else:
+      vc_toolset_dir = 'Microsoft.{}.DebugCRT' \
+         .format(MSVC_TOOLSET_VERSION[GetVisualStudioVersion()])
       source_dir = os.path.join(vc_redist_root, 'debug_nonredist',
-                                'arm64', 'Microsoft.VC141.DebugCRT')
-  for file_part in ('msvcp', 'vccorlib', 'vcruntime'):
-    dll = dll_pattern % file_part
+                                'arm64', vc_toolset_dir)
+  file_parts = ('msvcp140', 'vccorlib140', 'vcruntime140')
+  if target_cpu == 'x64' and GetVisualStudioVersion() != '2017':
+    file_parts = file_parts + ('vcruntime140_1', )
+  for file_part in file_parts:
+    dll = file_part + suffix
     target = os.path.join(target_dir, dll)
     source = os.path.join(source_dir, dll)
     _CopyRuntimeImpl(target, source)
@@ -317,14 +362,13 @@
   assert ('GYP_MSVS_OVERRIDE_PATH' in os.environ)
   vc_component_msvc_root = os.path.join(os.environ['GYP_MSVS_OVERRIDE_PATH'],
       'VC', component, 'MSVC')
-  vc_component_msvc_contents = os.listdir(vc_component_msvc_root)
+  vc_component_msvc_contents = glob.glob(
+      os.path.join(vc_component_msvc_root, '14.*'))
   # Select the most recent toolchain if there are several.
   _SortByHighestVersionNumberFirst(vc_component_msvc_contents)
   for directory in vc_component_msvc_contents:
-    if not os.path.isdir(os.path.join(vc_component_msvc_root, directory)):
-      continue
-    if re.match(r'14\.\d+\.\d+', directory):
-      return os.path.join(vc_component_msvc_root, directory)
+    if os.path.isdir(directory):
+      return directory
   raise Exception('Unable to find the VC %s directory.' % component)
 
 
@@ -342,8 +386,7 @@
   directory does exist. Handles VS 2015, 2017 and 2019."""
   suffix = 'd.dll' if debug else '.dll'
   # VS 2015, 2017 and 2019 use the same CRT DLLs.
-  _CopyUCRTRuntime(target_dir, source_dir, target_cpu, '%s140' + suffix,
-                    suffix)
+  _CopyUCRTRuntime(target_dir, source_dir, target_cpu, suffix)
 
 
 def CopyDlls(target_dir, configuration, target_cpu):
@@ -394,17 +437,20 @@
   # List of debug files that should be copied, the first element of the tuple is
   # the name of the file and the second indicates if it's optional.
   debug_files = [('dbghelp.dll', False), ('dbgcore.dll', True)]
+  # The UCRT is not a redistributable component on arm64.
+  if target_cpu != 'arm64':
+    debug_files.extend([('api-ms-win-downlevel-kernel32-l2-1-0.dll', False),
+                        ('api-ms-win-eventing-provider-l1-1-0.dll', False)])
   for debug_file, is_optional in debug_files:
     full_path = os.path.join(win_sdk_dir, 'Debuggers', target_cpu, debug_file)
     if not os.path.exists(full_path):
       if is_optional:
         continue
       else:
-        # TODO(crbug.com/773476): remove version requirement.
-        raise Exception('%s not found in "%s"\r\nYou must install the '
-                        '"Debugging Tools for Windows" feature from the Windows'
-                        ' 10 SDK.'
-                        % (debug_file, full_path))
+        raise Exception('%s not found in "%s"\r\nYou must install '
+                        'Windows 10 SDK version 10.0.20348.0 including the '
+                        '"Debugging Tools for Windows" feature.' %
+                        (debug_file, full_path))
     target_path = os.path.join(target_dir, debug_file)
     _CopyRuntimeImpl(target_path, full_path)
 
@@ -412,17 +458,10 @@
 def _GetDesiredVsToolchainHashes():
   """Load a list of SHA1s corresponding to the toolchains that we want installed
   to build with."""
-  env_version = GetVisualStudioVersion()
-  if env_version == '2017':
-    # VS 2017 Update 9 (15.9.12) with 10.0.18362 SDK, 10.0.17763 version of
-    # Debuggers, and 10.0.17134 version of d3dcompiler_47.dll, with ARM64
-    # libraries.
-    toolchain_hash = '418b3076791776573a815eb298c8aa590307af63'
-    # Third parties that do not have access to the canonical toolchain can map
-    # canonical toolchain version to their own toolchain versions.
-    toolchain_hash_mapping_key = 'GYP_MSVS_HASH_%s' % toolchain_hash
-    return [os.environ.get(toolchain_hash_mapping_key, toolchain_hash)]
-  raise Exception('Unsupported VS version %s' % env_version)
+  # Third parties that do not have access to the canonical toolchain can map
+  # canonical toolchain version to their own toolchain versions.
+  toolchain_hash_mapping_key = 'GYP_MSVS_HASH_%s' % TOOLCHAIN_HASH
+  return [os.environ.get(toolchain_hash_mapping_key, TOOLCHAIN_HASH)]
 
 
 def ShouldUpdateToolchain():
@@ -453,8 +492,7 @@
 
   depot_tools_win_toolchain = \
       bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1')))
-  if ((sys.platform in ('win32', 'cygwin') or force) and
-        depot_tools_win_toolchain):
+  if (_HostIsWindows() or force) and depot_tools_win_toolchain:
     import find_depot_tools
     depot_tools_path = find_depot_tools.add_depot_tools_to_path()
 
@@ -482,9 +520,6 @@
       subprocess.check_call([
           ciopfs, '-o', 'use_ino', toolchain_dir + '.ciopfs', toolchain_dir])
 
-    # Necessary so that get_toolchain_if_necessary.py will put the VS toolkit
-    # in the correct directory.
-    os.environ['GYP_MSVS_VERSION'] = GetVisualStudioVersion()
     get_toolchain_args = [
         sys.executable,
         os.path.join(depot_tools_path,