Changes to support RBE in Windows builds (#820)

This PR does a few things:
1. It changes how the Windows toolchain and SDK are fetched and where
they are stored. Previously, the Windows toolchain and SDK were
downloaded under `depot_tools`, which typically exists outside of the
engine source root both locally and in CI. That setup makes the Windows
SDK inaccessible to RBE builds. Instead, this PR checks out
`depot_tools` under `flutter/third_party/depot_tools` by listing it in
the `DEPS` file. This location is under the engine source root, and is
therefore accessible to RBE builds. This change has the downside of
breaking how CI does caching of the `depot_tools` Windows toolchain and
SDK, which will add some time to builds.
2. It updates Windows GN toolchain setup logic to match what is in the
Chrome tree. Updates to `build/find_depot_tools.py`,
`build/toolchain/win/BUILD.gn`, and
`build/toolchain/win/setup_toolchain.py` are copied from there and
adapted to the Flutter engine repo.
diff --git a/build/.gitignore b/build/.gitignore
index 48b9b2e..05efc47 100644
--- a/build/.gitignore
+++ b/build/.gitignore
@@ -1,3 +1,4 @@
 # Generated file containing information about the VS toolchain on Windows

 win_toolchain.json

+new_win_toolchain.json

 /linux/debian_*-sysroot/

diff --git a/build/config/BUILDCONFIG.gn b/build/config/BUILDCONFIG.gn
index fabc259..87ea4c5 100644
--- a/build/config/BUILDCONFIG.gn
+++ b/build/config/BUILDCONFIG.gn
@@ -526,13 +526,9 @@
   host_toolchain = "//build/toolchain/linux:clang_$host_cpu"
   set_default_toolchain("//build/toolchain/custom")
 } else if (is_win) {
-  if (is_clang) {
-    host_toolchain = "//build/toolchain/win:clang_$host_cpu"
-    set_default_toolchain("//build/toolchain/win:clang_$current_cpu")
-  } else {
-    host_toolchain = "//build/toolchain/win:$host_cpu"
-    set_default_toolchain("//build/toolchain/win:$current_cpu")
-  }
+  assert(is_clang)
+  host_toolchain = "//build/toolchain/win:clang_$host_cpu"
+  set_default_toolchain("//build/toolchain/win:clang_$current_cpu")
 } else if (is_android) {
   if (host_os == "linux") {
     # Use clang for the x86/64 Linux host builds.
@@ -544,11 +540,8 @@
   } else if (host_os == "mac") {
     host_toolchain = "//build/toolchain/mac:clang_$host_cpu"
   } else if (host_os == "win") {
-    if (is_clang) {
-      host_toolchain = "//build/toolchain/win:clang_$host_cpu"
-    } else {
-      host_toolchain = "//build/toolchain/win:$host_cpu"
-    }
+    assert(is_clang)
+    host_toolchain = "//build/toolchain/win:clang_$host_cpu"
   } else {
     assert(false, "Unknown host for android cross compile")
   }
diff --git a/build/find_depot_tools.py b/build/find_depot_tools.py
index 5d6661d..11e2f9a 100644
--- a/build/find_depot_tools.py
+++ b/build/find_depot_tools.py
@@ -1,23 +1,41 @@
-# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+#!/usr/bin/env python3
+# Copyright 2011 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 """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.
 """
 
+
 import os
 import sys
 
 
+# Path to //src
+SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
+
+
 def IsRealDepotTools(path):
-  return os.path.isfile(os.path.join(path, 'gclient.py'))
+  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 look if depot_tools is already in PYTHONPATH.
+  # First, check if we have a DEPS'd in "depot_tools".
+  deps_depot_tools = os.path.join(SRC, 'flutter', '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
@@ -39,7 +57,18 @@
   print('Failed to find depot_tools', file=sys.stderr)
   return None
 
-add_depot_tools_to_path()
+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 79269b7..46d28fb 100644
--- a/build/toolchain/win/BUILD.gn
+++ b/build/toolchain/win/BUILD.gn
@@ -2,6 +2,9 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/toolchain/rbe.gni")
+import("//build/toolchain/win/win_toolchain_data.gni")
+
 default_clang_base_path = "//buildtools/windows-x64/clang"
 
 declare_args() {
@@ -31,23 +34,18 @@
 # This tool will is used as a wrapper for various commands below.
 tool_wrapper_path = rebase_path("tool_wrapper.py", root_build_dir)
 
-toolchain_data = exec_script("setup_toolchain.py",
-                             [
-                               visual_studio_path,
-                               windows_sdk_path,
-                               visual_studio_runtime_dirs,
-                               current_cpu,
-                             ],
-                             "scope")
-
-if (vc_bin_dir == "") {
-  vc_bin_dir = toolchain_data.vc_bin_dir
-}
-
 if (use_goma) {
-  goma_prefix = "$goma_dir/gomacc.exe "
+  compiler_prefix = "$goma_dir/gomacc.exe "
+  asm_prefix = "$goma_dir/gomacc.exe "
+} else if (use_rbe) {
+  compiler_args = rewrapper_command + [
+    "--labels=type=compile,compiler=clang-cl,lang=cpp ",
+  ]
+  compiler_prefix = string_join(" ", compiler_args)
+  asm_prefix = " "
 } else {
-  goma_prefix = ""
+  compiler_prefix = ""
+  asm_prefix = ""
 }
 
 if (current_toolchain == default_toolchain) {
@@ -80,7 +78,18 @@
       }
     }
 
+    # TODO(zanderso): Assert that clang is always used or remove this logic.
+    if (defined(toolchain_args.is_clang)) {
+      toolchain_is_clang = toolchain_args.is_clang
+    } else {
+      toolchain_is_clang = is_clang
+    }
+
     env = invoker.environment
+    env_path = "$root_out_dir/$env"
+    print(env_path)
+    env_file = read_file(env_path, "string")
+    print(env_file)
 
     cl = invoker.cl
 
@@ -88,40 +97,54 @@
     lib_switch = ""
     lib_dir_switch = "/LIBPATH:"
 
-    tool("cc") {
-      rspfile = "{{output}}.rsp"
+    # If possible, pass system includes as flags to the compiler.  When that's
+    # not possible, load a full environment file (containing %INCLUDE% and
+    # %PATH%) -- e.g. 32-bit MSVS builds require %PATH% to be set and just
+    # passing in a list of include directories isn't enough.
+    if (defined(invoker.sys_include_flags)) {
+      assert(toolchain_is_clang)
+      env_wrapper = ""
+      sys_include_flags =
+          "${invoker.sys_include_flags} "  # Note trailing space.
+    } else {
+      # clang-cl doesn't need this env hoop, so omit it there.
+      assert(!toolchain_is_clang)
+      env_wrapper = "ninja -t msvc -e $env -- "  # Note trailing space.
+      sys_include_flags = ""
+    }
 
-      # TODO(brettw) enable this when GN support in the binary has been rolled.
-      #precompiled_header_type = "msvc"
-      pdbname = "{{target_out_dir}}/{{target_output_name}}_c.pdb"
-      command = "ninja -t msvc -e $env -- $cl /nologo /showIncludes @$rspfile /c {{source}} /Fo{{output}} /Fd$pdbname"
+    tool("cc") {
+      precompiled_header_type = "msvc"
+      pdbname = "{{target_out_dir}}/{{label_name}}_c.pdb"
       depsformat = "msvc"
       description = "CC {{output}}"
-      outputs = [
-        "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.obj",
-      ]
-      rspfile_content = "{{defines}} {{include_dirs}} {{cflags}} {{cflags_c}}"
+      outputs = [ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.obj" ]
+
+      # Label names may have spaces in them so the pdbname must be quoted. The
+      # source and output don't need to be quoted because GN knows they're a
+      # full file name and will quote automatically when necessary.
+      command = "$env_wrapper$cl /c {{source}} /nologo /showIncludes:user $sys_include_flags{{defines}} {{include_dirs}} {{cflags}} {{cflags_c}} /Fo{{output}} /Fd\"$pdbname\""
     }
 
     tool("cxx") {
-      rspfile = "{{output}}.rsp"
-
-      # TODO(brettw) enable this when GN support in the binary has been rolled.
-      #precompiled_header_type = "msvc"
+      precompiled_header_type = "msvc"
 
       # The PDB name needs to be different between C and C++ compiled files.
-      pdbname = "{{target_out_dir}}/{{target_output_name}}_cc.pdb"
+      pdbname = "{{target_out_dir}}/{{label_name}}_cc.pdb"
+
+      # TODO(zanderso): This logic should be moved to be with the other compiler
+      # flag logic in build/config/compiler/BUILD.gn or so.
       flags = ""
       if (is_clang && invoker.current_cpu == "x86") {
         flags = "-m32"
       }
-      command = "ninja -t msvc -e $env -- $cl $flags /nologo /showIncludes @$rspfile /c {{source}} /Fo{{output}} /Fd$pdbname"
+
       depsformat = "msvc"
       description = "CXX {{output}}"
-      outputs = [
-        "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.obj",
-      ]
-      rspfile_content = "{{defines}} {{include_dirs}} {{cflags}} {{cflags_cc}}"
+      outputs = [ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.obj" ]
+
+      # See comment in CC tool about quoting.
+      command = "$env_wrapper$cl /c {{source}} /Fo{{output}} $flags /nologo /showIncludes:user $sys_include_flags{{defines}} {{include_dirs}} {{cflags}} {{cflags_cc}} /Fd\"$pdbname\""
     }
 
     tool("rc") {
@@ -141,7 +164,7 @@
       } else if (toolchain_args.current_cpu == "arm64") {
         is_msvc_assembler = false
         prefix = rebase_path("$clang_base_path/bin", root_build_dir)
-        ml = "${goma_prefix}${prefix}/clang-cl.exe --target=arm64-windows"
+        ml = "${compiler_prefix}${prefix}/clang-cl.exe --target=arm64-windows"
         x64 = ""
       } else {
         ml = "ml.exe"
@@ -175,14 +198,11 @@
     }
 
     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
-      expname = "{{root_out_dir}}/{{target_output_name}}{{output_extension}}.exp"  # e.g.
-                                                                                   # foo.dll.exp
-      pdbname = "{{root_out_dir}}/{{target_output_name}}{{output_extension}}.pdb"  # e.g.
-                                                                                   # foo.dll.pdb
+      # E.g. "foo.dll":
+      dllname = "{{root_out_dir}}/{{target_output_name}}{{output_extension}}"
+      libname = "${dllname}.lib"  # e.g. foo.dll.lib
+      expname = "${dllname}.exp"
+      pdbname = "${dllname}.pdb"
       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"
@@ -253,21 +273,22 @@
   assert(defined(invoker.toolchain_arch))
   toolchain_arch = invoker.toolchain_arch
 
-  msvc_toolchain(target_name) {
-    environment = "environment." + toolchain_arch
-    cl = "${goma_prefix}\"${vc_bin_dir}/cl.exe\""
-    toolchain_args = {
-      if (defined(invoker.toolchain_args)) {
-        forward_variables_from(invoker.toolchain_args, "*")
-      }
-      current_cpu = toolchain_arch
-      is_clang = false
-    }
+  # The toolchain data for `msvc_toolchain()`.
+  if (toolchain_arch == "x86") {
+    win_toolchain_data = win_toolchain_data_x86
+  } else if (toolchain_arch == "x64") {
+    win_toolchain_data = win_toolchain_data_x64
+  } else if (toolchain_arch == "arm64") {
+    win_toolchain_data = win_toolchain_data_arm64
+  } else {
+    error("Unsupported toolchain_arch, add it to win_toolchain_data.gni")
   }
+
   msvc_toolchain("clang_" + target_name) {
     environment = "environment." + toolchain_arch
     prefix = rebase_path("$clang_base_path/bin", root_build_dir)
-    cl = "${goma_prefix}$prefix/clang-cl.exe"
+    cl = "${compiler_prefix}$prefix/clang-cl.exe"
+    sys_include_flags = "${win_toolchain_data.include_flags_imsvc}"
     toolchain_args = {
       if (defined(invoker.toolchain_args)) {
         forward_variables_from(invoker.toolchain_args, "*")
diff --git a/build/toolchain/win/setup_toolchain.py b/build/toolchain/win/setup_toolchain.py
index dbbee74..521c243 100644
--- a/build/toolchain/win/setup_toolchain.py
+++ b/build/toolchain/win/setup_toolchain.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Copyright 2013 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 #
@@ -10,69 +10,184 @@
 # win tool. The script assumes that the root build directory is the current dir
 # and the files will be written to the current directory.
 
+
 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__)
+SDK_VERSION = '10.0.22621.0'
+
 
 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.
-  accepted_cpus = ('x86', 'x64', 'arm64')
-  assert target_cpu in accepted_cpus, '%s not in accepted cpus %s' % (target_cpu, accepted_cpus)
+  # 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:
-    vcvars_arch = {
-        'x86': 'amd64_x86',
-        'x64': 'amd64',
-        'arm64': 'amd64_arm64',
-    }
+    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')),
-            vcvars_arch[target_cpu]]
+    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(SDK_VERSION)
+    variables = _LoadEnvFromBat(args)
+  return _ExtractImportantEnvironment(variables)
 
 
 def _FormatAsEnvironmentBlock(envvar_dict):
@@ -87,69 +202,126 @@
   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)
-    variables, _ = popen.communicate()
-    env = _ExtractImportantEnvironment(variables)
-    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 = ['/I' + i for i in include]
+      include_imsvc = ['-imsvc' + i for i in include]
+      libpath_flags = ['-libpath:' + i for i in lib]
+
+      if (environment_block_name != ''):
+        env_block = _FormatAsEnvironmentBlock(env)
+        with open(environment_block_name, 'w', encoding='utf8') as f:
+          f.write(env_block)
+
+  def ListToArgString(x):
+    return gn_helpers.ToGNString(' '.join(q(i) for i in x))
+
+  def ListToArgList(x):
+    return f'[{", ".join(gn_helpers.ToGNString(i) for i in x)}]'
+
+  print('vc_bin_dir = ' + gn_helpers.ToGNString(vc_bin_dir))
+  assert include_I
+  print(f'include_flags_I = {ListToArgString(include_I)}')
+  print(f'include_flags_I_list = {ListToArgList(include_I)}')
+  assert include_imsvc
+  if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and win_sdk_path:
+    flags = ['/winsysroot' + relflag(toolchain_root)]
+    print(f'include_flags_imsvc = {ListToArgString(flags)}')
+    print(f'include_flags_imsvc_list = {ListToArgList(flags)}')
+  else:
+    print(f'include_flags_imsvc = {ListToArgString(include_imsvc)}')
+    print(f'include_flags_imsvc_list = {ListToArgList(include_imsvc)}')
+  print('paths = ' + gn_helpers.ToGNString(env['PATH']))
+  assert libpath_flags
+  print(f'libpath_flags = {ListToArgString(libpath_flags)}')
+  print(f'libpath_flags_list = {ListToArgList(libpath_flags)}')
+  if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and win_sdk_path:
+    flags = ['/winsysroot:' + relflag(toolchain_root)]
+    print(f'libpath_lldlink_flags = {ListToArgString(flags)}')
+    print(f'libpath_lldlink_flags_list = {ListToArgList(flags)}')
+  else:
+    print(f'libpath_lldlink_flags = {ListToArgString(libpath_flags)}')
+    print(f'libpath_lldlink_flags_list = {ListToArgList(libpath_flags)}')
 
 
 if __name__ == '__main__':
diff --git a/build/toolchain/win/win_toolchain_data.gni b/build/toolchain/win/win_toolchain_data.gni
new file mode 100644
index 0000000..a6e8328
--- /dev/null
+++ b/build/toolchain/win/win_toolchain_data.gni
@@ -0,0 +1,44 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/win/visual_studio_version.gni")
+
+declare_args() {
+  win_toolchain_data_x86 =
+      exec_script("//build/toolchain/win/setup_toolchain.py",
+                  [
+                    visual_studio_path,
+                    windows_sdk_path,
+                    visual_studio_runtime_dirs,
+                    "win",
+                    "x86",
+                    "environment.x86",
+                  ],
+                  "scope")
+
+  win_toolchain_data_x64 =
+      exec_script("//build/toolchain/win/setup_toolchain.py",
+                  [
+                    visual_studio_path,
+                    windows_sdk_path,
+                    visual_studio_runtime_dirs,
+                    "win",
+                    "x64",
+                    "environment.x64",
+                  ],
+                  "scope")
+  if (target_cpu == "arm64" || host_cpu == "arm64") {
+    win_toolchain_data_arm64 =
+        exec_script("//build/toolchain/win/setup_toolchain.py",
+                    [
+                      visual_studio_path,
+                      windows_sdk_path,
+                      visual_studio_runtime_dirs,
+                      "win",
+                      "arm64",
+                      "environment.arm64",
+                    ],
+                    "scope")
+  }
+}
diff --git a/build/vs_toolchain.py b/build/vs_toolchain.py
index 9625a1d..599b55a 100644
--- a/build/vs_toolchain.py
+++ b/build/vs_toolchain.py
@@ -30,7 +30,7 @@
 from gn_helpers import ToGNString
 
 script_dir = os.path.dirname(os.path.realpath(__file__))
-json_data_file = os.path.join(script_dir, 'win_toolchain.json')
+json_data_file = os.path.join(script_dir, 'new_win_toolchain.json')
 
 sys.path.insert(0, os.path.join(script_dir))
 
@@ -496,6 +496,7 @@
                     'win_toolchain',
                     'get_toolchain_if_necessary.py'),
         '--output-json', json_data_file,
+        '--toolchain-dir', os.path.join(depot_tools_path, 'win_toolchain'),
       ] + _GetDesiredVsToolchainHashes()
     if force:
       get_toolchain_args.append('--force')