xref: /aosp_15_r20/external/angle/build/toolchain/win/setup_toolchain.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1# Copyright 2013 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4#
5# Copies the given "win tool" (which the toolchain uses to wrap compiler
6# invocations) and the environment blocks for the 32-bit and 64-bit builds on
7# Windows to the build directory.
8#
9# The arguments are the visual studio install location and the location of the
10# win tool. The script assumes that the root build directory is the current dir
11# and the files will be written to the current directory.
12
13
14import errno
15import json
16import os
17import re
18import subprocess
19import sys
20
21sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
22import gn_helpers
23
24SCRIPT_DIR = os.path.dirname(__file__)
25SDK_VERSION = '10.0.22621.0'
26
27
28def _ExtractImportantEnvironment(output_of_set):
29  """Extracts environment variables required for the toolchain to run from
30  a textual dump output by the cmd.exe 'set' command."""
31  envvars_to_save = (
32      'cipd_cache_dir',  # needed by vpython
33      'homedrive',  # needed by vpython
34      'homepath',  # needed by vpython
35      'include',
36      'lib',
37      'libpath',
38      'luci_context',  # needed by vpython
39      'path',
40      'pathext',
41      'systemroot',
42      'temp',
43      'tmp',
44      'userprofile',  # needed by vpython
45      'vpython_virtualenv_root'  # needed by vpython
46  )
47  env = {}
48  # This occasionally happens and leads to misleading SYSTEMROOT error messages
49  # if not caught here.
50  if output_of_set.count('=') == 0:
51    raise Exception('Invalid output_of_set. Value is:\n%s' % output_of_set)
52  for line in output_of_set.splitlines():
53    for envvar in envvars_to_save:
54      if re.match(envvar + '=', line.lower()):
55        var, setting = line.split('=', 1)
56        if envvar == 'path':
57          # Our own rules and actions in Chromium rely on python being in the
58          # path. Add the path to this python here so that if it's not in the
59          # path when ninja is run later, python will still be found.
60          setting = os.path.dirname(sys.executable) + os.pathsep + setting
61        if envvar in ['include', 'lib']:
62          # Make sure that the include and lib paths point to directories that
63          # exist. This ensures a (relatively) clear error message if the
64          # required SDK is not installed.
65          for part in setting.split(';'):
66            if not os.path.exists(part) and len(part) != 0:
67              raise Exception(
68                  'Path "%s" from environment variable "%s" does not exist. '
69                  'Make sure the necessary SDK is installed.' % (part, envvar))
70        env[var.upper()] = setting
71        break
72  if sys.platform in ('win32', 'cygwin'):
73    for required in ('SYSTEMROOT', 'TEMP', 'TMP'):
74      if required not in env:
75        raise Exception('Environment variable "%s" '
76                        'required to be set to valid path' % required)
77  return env
78
79
80def _DetectVisualStudioPath():
81  """Return path to the installed Visual Studio.
82  """
83
84  # Use the code in build/vs_toolchain.py to avoid duplicating code.
85  chromium_dir = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..', '..'))
86  sys.path.append(os.path.join(chromium_dir, 'build'))
87  import vs_toolchain
88  return vs_toolchain.DetectVisualStudioPath()
89
90
91def _LoadEnvFromBat(args):
92  """Given a bat command, runs it and returns env vars set by it."""
93  args = args[:]
94  args.extend(('&&', 'set'))
95  popen = subprocess.Popen(
96      args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
97  variables, _ = popen.communicate()
98  if popen.returncode != 0:
99    raise Exception('"%s" failed with error %d' % (args, popen.returncode))
100  return variables.decode(errors='ignore')
101
102
103def _LoadToolchainEnv(cpu, toolchain_root, sdk_dir, target_store):
104  """Returns a dictionary with environment variables that must be set while
105  running binaries from the toolchain (e.g. INCLUDE and PATH for cl.exe)."""
106  # Check if we are running in the SDK command line environment and use
107  # the setup script from the SDK if so. |cpu| should be either
108  # 'x86' or 'x64' or 'arm' or 'arm64'.
109  assert cpu in ('x86', 'x64', 'arm', 'arm64')
110  if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and sdk_dir:
111    # Load environment from json file.
112    env = os.path.normpath(os.path.join(sdk_dir, 'bin/SetEnv.%s.json' % cpu))
113    env = json.load(open(env))['env']
114    if env['VSINSTALLDIR'] == [["..", "..\\"]]:
115      # Old-style paths were relative to the win_sdk\bin directory.
116      json_relative_dir = os.path.join(sdk_dir, 'bin')
117    else:
118      # New-style paths are relative to the toolchain directory.
119      json_relative_dir = toolchain_root
120    for k in env:
121      entries = [os.path.join(*([json_relative_dir] + e)) for e in env[k]]
122      # clang-cl wants INCLUDE to be ;-separated even on non-Windows,
123      # lld-link wants LIB to be ;-separated even on non-Windows.  Path gets :.
124      # The separator for INCLUDE here must match the one used in main() below.
125      sep = os.pathsep if k == 'PATH' else ';'
126      env[k] = sep.join(entries)
127    # PATH is a bit of a special case, it's in addition to the current PATH.
128    env['PATH'] = env['PATH'] + os.pathsep + os.environ['PATH']
129    # Augment with the current env to pick up TEMP and friends.
130    for k in os.environ:
131      if k not in env:
132        env[k] = os.environ[k]
133
134    varlines = []
135    for k in sorted(env.keys()):
136      varlines.append('%s=%s' % (str(k), str(env[k])))
137    variables = '\n'.join(varlines)
138
139    # Check that the json file contained the same environment as the .cmd file.
140    if sys.platform in ('win32', 'cygwin'):
141      script = os.path.normpath(os.path.join(sdk_dir, 'Bin/SetEnv.cmd'))
142      arg = '/' + cpu
143      json_env = _ExtractImportantEnvironment(variables)
144      cmd_env = _ExtractImportantEnvironment(_LoadEnvFromBat([script, arg]))
145      assert _LowercaseDict(json_env) == _LowercaseDict(cmd_env)
146  else:
147    if 'GYP_MSVS_OVERRIDE_PATH' not in os.environ:
148      os.environ['GYP_MSVS_OVERRIDE_PATH'] = _DetectVisualStudioPath()
149    # We only support x64-hosted tools.
150    script_path = os.path.normpath(os.path.join(
151                                       os.environ['GYP_MSVS_OVERRIDE_PATH'],
152                                       'VC/vcvarsall.bat'))
153    if not os.path.exists(script_path):
154      # vcvarsall.bat for VS 2017 fails if run after running vcvarsall.bat from
155      # VS 2013 or VS 2015. Fix this by clearing the vsinstalldir environment
156      # variable. Since vcvarsall.bat appends to the INCLUDE, LIB, and LIBPATH
157      # environment variables we need to clear those to avoid getting double
158      # entries when vcvarsall.bat has been run before gn gen. vcvarsall.bat
159      # also adds to PATH, but there is no clean way of clearing that and it
160      # doesn't seem to cause problems.
161      if 'VSINSTALLDIR' in os.environ:
162        del os.environ['VSINSTALLDIR']
163        if 'INCLUDE' in os.environ:
164          del os.environ['INCLUDE']
165        if 'LIB' in os.environ:
166          del os.environ['LIB']
167        if 'LIBPATH' in os.environ:
168          del os.environ['LIBPATH']
169      other_path = os.path.normpath(os.path.join(
170                                        os.environ['GYP_MSVS_OVERRIDE_PATH'],
171                                        'VC/Auxiliary/Build/vcvarsall.bat'))
172      if not os.path.exists(other_path):
173        raise Exception('%s is missing - make sure VC++ tools are installed.' %
174                        script_path)
175      script_path = other_path
176    cpu_arg = "amd64"
177    if (cpu != 'x64'):
178      # x64 is default target CPU thus any other CPU requires a target set
179      cpu_arg += '_' + cpu
180    args = [script_path, cpu_arg, ]
181    # Store target must come before any SDK version declaration
182    if (target_store):
183      args.append('store')
184    # Explicitly specifying the SDK version to build with to avoid accidentally
185    # building with a new and untested SDK. This should stay in sync with the
186    # packaged toolchain in build/vs_toolchain.py.
187    args.append(SDK_VERSION)
188    variables = _LoadEnvFromBat(args)
189  return _ExtractImportantEnvironment(variables)
190
191
192def _FormatAsEnvironmentBlock(envvar_dict):
193  """Format as an 'environment block' directly suitable for CreateProcess.
194  Briefly this is a list of key=value\0, terminated by an additional \0. See
195  CreateProcess documentation for more details."""
196  block = ''
197  nul = '\0'
198  for key, value in envvar_dict.items():
199    block += key + '=' + value + nul
200  block += nul
201  return block
202
203
204def _LowercaseDict(d):
205  """Returns a copy of `d` with both key and values lowercased.
206
207  Args:
208    d: dict to lowercase (e.g. {'A': 'BcD'}).
209
210  Returns:
211    A dict with both keys and values lowercased (e.g.: {'a': 'bcd'}).
212  """
213  return {k.lower(): d[k].lower() for k in d}
214
215
216def FindFileInEnvList(env, env_name, separator, file_name, optional=False):
217  parts = env[env_name].split(separator)
218  for path in parts:
219    if os.path.exists(os.path.join(path, file_name)):
220      return os.path.realpath(path)
221  assert optional, "%s is not found in %s:\n%s\nCheck if it is installed." % (
222      file_name, env_name, '\n'.join(parts))
223  return ''
224
225
226def main():
227  if len(sys.argv) != 7:
228    print('Usage setup_toolchain.py '
229          '<visual studio path> <win sdk path> '
230          '<runtime dirs> <target_os> <target_cpu> '
231          '<environment block name|none>')
232    sys.exit(2)
233  # toolchain_root and win_sdk_path are only read if the hermetic Windows
234  # toolchain is set, that is if DEPOT_TOOLS_WIN_TOOLCHAIN is not set to 0.
235  # With the hermetic Windows toolchain, the visual studio path in argv[1]
236  # is the root of the Windows toolchain directory.
237  toolchain_root = sys.argv[1]
238  win_sdk_path = sys.argv[2]
239
240  runtime_dirs = sys.argv[3]
241  target_os = sys.argv[4]
242  target_cpu = sys.argv[5]
243  environment_block_name = sys.argv[6]
244  if (environment_block_name == 'none'):
245    environment_block_name = ''
246
247  if (target_os == 'winuwp'):
248    target_store = True
249  else:
250    target_store = False
251
252  cpus = ('x86', 'x64', 'arm', 'arm64')
253  assert target_cpu in cpus
254  vc_bin_dir = ''
255  include = ''
256  lib = ''
257
258  def relflag(s):  # Make s relative to builddir when cwd and sdk on same drive.
259    try:
260      return os.path.relpath(s).replace('\\', '/')
261    except ValueError:
262      return s
263
264  def q(s):  # Quote s if it contains spaces or other weird characters.
265    return s if re.match(r'^[a-zA-Z0-9._/\\:-]*$', s) else '"' + s + '"'
266
267  for cpu in cpus:
268    if cpu == target_cpu:
269      # Extract environment variables for subprocesses.
270      env = _LoadToolchainEnv(cpu, toolchain_root, win_sdk_path, target_store)
271      env['PATH'] = runtime_dirs + os.pathsep + env['PATH']
272
273      vc_bin_dir = FindFileInEnvList(env, 'PATH', os.pathsep, 'cl.exe')
274
275      # The separator for INCLUDE here must match the one used in
276      # _LoadToolchainEnv() above.
277      include = [p.replace('"', r'\"') for p in env['INCLUDE'].split(';') if p]
278      include = list(map(relflag, include))
279
280      lib = [p.replace('"', r'\"') for p in env['LIB'].split(';') if p]
281      lib = list(map(relflag, lib))
282
283      include_I = ['/I' + i for i in include]
284      include_imsvc = ['-imsvc' + i for i in include]
285      libpath_flags = ['-libpath:' + i for i in lib]
286
287      if (environment_block_name != ''):
288        env_block = _FormatAsEnvironmentBlock(env)
289        with open(environment_block_name, 'w', encoding='utf8') as f:
290          f.write(env_block)
291
292  def ListToArgString(x):
293    return gn_helpers.ToGNString(' '.join(q(i) for i in x))
294
295  def ListToArgList(x):
296    return f'[{", ".join(gn_helpers.ToGNString(i) for i in x)}]'
297
298  print('vc_bin_dir = ' + gn_helpers.ToGNString(vc_bin_dir))
299  assert include_I
300  print(f'include_flags_I = {ListToArgString(include_I)}')
301  print(f'include_flags_I_list = {ListToArgList(include_I)}')
302  assert include_imsvc
303  if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and win_sdk_path:
304    flags = ['/winsysroot' + relflag(toolchain_root)]
305    print(f'include_flags_imsvc = {ListToArgString(flags)}')
306    print(f'include_flags_imsvc_list = {ListToArgList(flags)}')
307  else:
308    print(f'include_flags_imsvc = {ListToArgString(include_imsvc)}')
309    print(f'include_flags_imsvc_list = {ListToArgList(include_imsvc)}')
310  print('paths = ' + gn_helpers.ToGNString(env['PATH']))
311  assert libpath_flags
312  print(f'libpath_flags = {ListToArgString(libpath_flags)}')
313  print(f'libpath_flags_list = {ListToArgList(libpath_flags)}')
314  if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and win_sdk_path:
315    flags = ['/winsysroot:' + relflag(toolchain_root)]
316    print(f'libpath_lldlink_flags = {ListToArgString(flags)}')
317    print(f'libpath_lldlink_flags_list = {ListToArgList(flags)}')
318  else:
319    print(f'libpath_lldlink_flags = {ListToArgString(libpath_flags)}')
320    print(f'libpath_lldlink_flags_list = {ListToArgList(libpath_flags)}')
321
322
323if __name__ == '__main__':
324  main()
325