xref: /aosp_15_r20/external/angle/build/vs_toolchain.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1#!/usr/bin/env python3
2# Copyright 2014 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6
7import collections
8import glob
9import json
10import os
11import platform
12import re
13import shutil
14import stat
15import subprocess
16import sys
17
18from gn_helpers import ToGNString
19
20# VS 2022 17.9.2 with 10.0.22621.2428 SDK with ARM64 libraries and UWP support.
21# See go/win-toolchain-reference for instructions about how to update the
22# toolchain.
23#
24# When updating the toolchain, consider the following areas impacted by the
25# toolchain version:
26#
27# * This file -- SDK_VERSION and TOOLCHAIN_HASH
28#   Determines which version of the toolchain is used by gclient. The hash
29#   is the name of the toolchain package (minus the zip) in gcloud, and
30#   SDK_VERSION should match the SDK version in that package.
31#
32# * This file -- MSVS_VERSIONS
33#   Records the supported versions of Visual Studio, in priority order.
34#
35# * This file -- MSVC_TOOLSET_VERSION
36#   Determines the expected MSVC toolset for each version of Visual Studio.
37#   The packaged toolset version can be seen at <package>/VC/redist/MSVC;
38#   there will be a folder named `v143` or similar.
39#
40# * build/toolchain/win/setup_toolchain.py -- SDK_VERSION
41#   Secondary specification of the SDK Version, to make sure we're loading the
42#   right one. Should always match SDK_VERSION in this file.
43#
44# * base/win/windows_version.cc -- NTDDI preprocessor check
45#   Forces developers to have a specific SDK version (or newer). Triggers a
46#   compiler error if the available SDK is older than the minimum.
47#
48# * build/config/win/BUILD.gn -- NTDDI_VERSION
49#   Specifies which SDK/WDK version is installed. Some of the toolchain headers
50#   check this macro to conditionally compile code.
51#
52# * build/config/win/BUILD.gn -- WINVER and _WIN32_WINNT
53#   Specify the minimum supported Windows version. These very rarely need to
54#   be changed.
55#
56# * tools/win/setenv.py -- list of accepted `vs_version`s
57#   Add/remove VS versions when upgrading to a new VS version.
58#
59# * docs/windows_build_instructions.md
60#   Make sure any version numbers in the documentation match the code.
61#
62TOOLCHAIN_HASH = '7393122652'
63SDK_VERSION = '10.0.22621.0'
64
65# Visual Studio versions are listed in descending order of priority.
66# The first version is assumed by this script to be the one that is packaged,
67# which makes a difference for the arm64 runtime.
68# The second number is an alternate version number, only used in an error string
69MSVS_VERSIONS = collections.OrderedDict([
70    ('2022', '17.0'),  # The VS version in our packaged toolchain.
71    ('2019', '16.0'),
72    ('2017', '15.0'),
73])
74
75# List of preferred VC toolset version based on MSVS
76# Order is not relevant for this dictionary.
77MSVC_TOOLSET_VERSION = {
78    '2022': 'VC143',
79    '2019': 'VC142',
80    '2017': 'VC141',
81}
82
83script_dir = os.path.dirname(os.path.realpath(__file__))
84json_data_file = os.path.join(script_dir, 'win_toolchain.json')
85
86
87def _HostIsWindows():
88  """Returns True if running on a Windows host (including under cygwin)."""
89  return sys.platform in ('win32', 'cygwin')
90
91def SetEnvironmentAndGetRuntimeDllDirs():
92  """Sets up os.environ to use the depot_tools VS toolchain with gyp, and
93  returns the location of the VC runtime DLLs so they can be copied into
94  the output directory after gyp generation.
95
96  Return value is [x64path, x86path, 'Arm64Unused'] or None. arm64path is
97  generated separately because there are multiple folders for the arm64 VC
98  runtime.
99  """
100  vs_runtime_dll_dirs = None
101  depot_tools_win_toolchain = \
102      bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1')))
103  # When running on a non-Windows host, only do this if the SDK has explicitly
104  # been downloaded before (in which case json_data_file will exist).
105  if ((_HostIsWindows() or os.path.exists(json_data_file))
106      and depot_tools_win_toolchain):
107    if ShouldUpdateToolchain():
108      if len(sys.argv) > 1 and sys.argv[1] == 'update':
109        update_result = Update()
110      else:
111        update_result = Update(no_download=True)
112      if update_result != 0:
113        raise Exception('Failed to update, error code %d.' % update_result)
114    with open(json_data_file, 'r') as tempf:
115      toolchain_data = json.load(tempf)
116
117    toolchain = toolchain_data['path']
118    version = toolchain_data['version']
119    win_sdk = toolchain_data.get('win_sdk')
120    wdk = toolchain_data['wdk']
121    # TODO(scottmg): The order unfortunately matters in these. They should be
122    # split into separate keys for x64/x86/arm64. (See CopyDlls call below).
123    # http://crbug.com/345992
124    vs_runtime_dll_dirs = toolchain_data['runtime_dirs']
125    # The number of runtime_dirs in the toolchain_data was two (x64/x86) but
126    # changed to three (x64/x86/arm64) and this code needs to handle both
127    # possibilities, which can change independently from this code.
128    if len(vs_runtime_dll_dirs) == 2:
129      vs_runtime_dll_dirs.append('Arm64Unused')
130
131    os.environ['GYP_MSVS_OVERRIDE_PATH'] = toolchain
132
133    os.environ['WINDOWSSDKDIR'] = win_sdk
134    os.environ['WDK_DIR'] = wdk
135    # Include the VS runtime in the PATH in case it's not machine-installed.
136    runtime_path = os.path.pathsep.join(vs_runtime_dll_dirs)
137    os.environ['PATH'] = runtime_path + os.path.pathsep + os.environ['PATH']
138  elif sys.platform == 'win32' and not depot_tools_win_toolchain:
139    if not 'GYP_MSVS_OVERRIDE_PATH' in os.environ:
140      os.environ['GYP_MSVS_OVERRIDE_PATH'] = DetectVisualStudioPath()
141
142    # When using an installed toolchain these files aren't needed in the output
143    # directory in order to run binaries locally, but they are needed in order
144    # to create isolates or the mini_installer. Copying them to the output
145    # directory ensures that they are available when needed.
146    bitness = platform.architecture()[0]
147    # When running 64-bit python the x64 DLLs will be in System32
148    # ARM64 binaries will not be available in the system directories because we
149    # don't build on ARM64 machines.
150    x64_path = 'System32' if bitness == '64bit' else 'Sysnative'
151    x64_path = os.path.join(os.path.expandvars('%windir%'), x64_path)
152    vs_runtime_dll_dirs = [x64_path,
153                           os.path.join(os.path.expandvars('%windir%'),
154                                        'SysWOW64'),
155                           'Arm64Unused']
156
157  return vs_runtime_dll_dirs
158
159
160def _RegistryGetValueUsingWinReg(key, value):
161  """Use the _winreg module to obtain the value of a registry key.
162
163  Args:
164    key: The registry key.
165    value: The particular registry value to read.
166  Return:
167    contents of the registry key's value, or None on failure.  Throws
168    ImportError if _winreg is unavailable.
169  """
170  import _winreg
171  try:
172    root, subkey = key.split('\\', 1)
173    assert root == 'HKLM'  # Only need HKLM for now.
174    with _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, subkey) as hkey:
175      return _winreg.QueryValueEx(hkey, value)[0]
176  except WindowsError:
177    return None
178
179
180def _RegistryGetValue(key, value):
181  try:
182    return _RegistryGetValueUsingWinReg(key, value)
183  except ImportError:
184    raise Exception('The python library _winreg not found.')
185
186
187def GetVisualStudioVersion():
188  """Return best available version of Visual Studio.
189  """
190  supported_versions = list(MSVS_VERSIONS.keys())
191
192  # VS installed in depot_tools for Googlers
193  if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1'))):
194    return supported_versions[0]
195
196  # VS installed in system for external developers
197  supported_versions_str = ', '.join('{} ({})'.format(v,k)
198      for k,v in MSVS_VERSIONS.items())
199  available_versions = []
200  for version in supported_versions:
201    # Checking vs%s_install environment variables.
202    # For example, vs2019_install could have the value
203    # "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community".
204    # Only vs2017_install, vs2019_install and vs2022_install are supported.
205    path = os.environ.get('vs%s_install' % version)
206    if path and os.path.exists(path):
207      available_versions.append(version)
208      break
209    # Detecting VS under possible paths.
210    if version >= '2022':
211      program_files_path_variable = '%ProgramFiles%'
212    else:
213      program_files_path_variable = '%ProgramFiles(x86)%'
214    path = os.path.expandvars(program_files_path_variable +
215                              '/Microsoft Visual Studio/%s' % version)
216    if path and any(
217        os.path.exists(os.path.join(path, edition))
218        for edition in ('Enterprise', 'Professional', 'Community', 'Preview',
219                        'BuildTools')):
220      available_versions.append(version)
221      break
222
223  if not available_versions:
224    raise Exception('No supported Visual Studio can be found.'
225                    ' Supported versions are: %s.' % supported_versions_str)
226  return available_versions[0]
227
228
229def DetectVisualStudioPath():
230  """Return path to the installed Visual Studio.
231  """
232
233  # Note that this code is used from
234  # build/toolchain/win/setup_toolchain.py as well.
235  version_as_year = GetVisualStudioVersion()
236
237  # The VC++ >=2017 install location needs to be located using COM instead of
238  # the registry. For details see:
239  # https://blogs.msdn.microsoft.com/heaths/2016/09/15/changes-to-visual-studio-15-setup/
240  # For now we use a hardcoded default with an environment variable override.
241  if version_as_year >= '2022':
242    program_files_path_variable = '%ProgramFiles%'
243  else:
244    program_files_path_variable = '%ProgramFiles(x86)%'
245  for path in (os.environ.get('vs%s_install' % version_as_year),
246               os.path.expandvars(program_files_path_variable +
247                                  '/Microsoft Visual Studio/%s/Enterprise' %
248                                  version_as_year),
249               os.path.expandvars(program_files_path_variable +
250                                  '/Microsoft Visual Studio/%s/Professional' %
251                                  version_as_year),
252               os.path.expandvars(program_files_path_variable +
253                                  '/Microsoft Visual Studio/%s/Community' %
254                                  version_as_year),
255               os.path.expandvars(program_files_path_variable +
256                                  '/Microsoft Visual Studio/%s/Preview' %
257                                  version_as_year),
258               os.path.expandvars(program_files_path_variable +
259                                  '/Microsoft Visual Studio/%s/BuildTools' %
260                                  version_as_year)):
261    if path and os.path.exists(path):
262      return path
263
264  raise Exception('Visual Studio Version %s not found.' % version_as_year)
265
266
267def _CopyRuntimeImpl(target, source, verbose=True):
268  """Copy |source| to |target| if it doesn't already exist or if it needs to be
269  updated (comparing last modified time as an approximate float match as for
270  some reason the values tend to differ by ~1e-07 despite being copies of the
271  same file... https://crbug.com/603603).
272  """
273  if (os.path.isdir(os.path.dirname(target)) and
274      (not os.path.isfile(target) or
275       abs(os.stat(target).st_mtime - os.stat(source).st_mtime) >= 0.01)):
276    if verbose:
277      print('Copying %s to %s...' % (source, target))
278    if os.path.exists(target):
279      # Make the file writable so that we can delete it now, and keep it
280      # readable.
281      os.chmod(target, stat.S_IWRITE | stat.S_IREAD)
282      os.unlink(target)
283    shutil.copy2(source, target)
284    # Make the file writable so that we can overwrite or delete it later,
285    # keep it readable.
286    os.chmod(target, stat.S_IWRITE | stat.S_IREAD)
287
288def _SortByHighestVersionNumberFirst(list_of_str_versions):
289  """This sorts |list_of_str_versions| according to version number rules
290  so that version "1.12" is higher than version "1.9". Does not work
291  with non-numeric versions like 1.4.a8 which will be higher than
292  1.4.a12. It does handle the versions being embedded in file paths.
293  """
294  def to_int_if_int(x):
295    try:
296      return int(x)
297    except ValueError:
298      return x
299
300  def to_number_sequence(x):
301    part_sequence = re.split(r'[\\/\.]', x)
302    return [to_int_if_int(x) for x in part_sequence]
303
304  list_of_str_versions.sort(key=to_number_sequence, reverse=True)
305
306
307def _CopyUCRTRuntime(target_dir, source_dir, target_cpu, suffix):
308  """Copy both the msvcp and vccorlib runtime DLLs, only if the target doesn't
309  exist, but the target directory does exist."""
310  if target_cpu == 'arm64':
311    # Windows ARM64 VCRuntime is located at {toolchain_root}/VC/Redist/MSVC/
312    # {x.y.z}/[debug_nonredist/]arm64/Microsoft.VC14x.CRT/.
313    # Select VC toolset directory based on Visual Studio version
314    vc_redist_root = FindVCRedistRoot()
315    if suffix.startswith('.'):
316      vc_toolset_dir = 'Microsoft.{}.CRT' \
317         .format(MSVC_TOOLSET_VERSION[GetVisualStudioVersion()])
318      source_dir = os.path.join(vc_redist_root,
319                                'arm64', vc_toolset_dir)
320    else:
321      vc_toolset_dir = 'Microsoft.{}.DebugCRT' \
322         .format(MSVC_TOOLSET_VERSION[GetVisualStudioVersion()])
323      source_dir = os.path.join(vc_redist_root, 'debug_nonredist',
324                                'arm64', vc_toolset_dir)
325  file_parts = ('msvcp140', 'vccorlib140', 'vcruntime140')
326  if target_cpu == 'x64' and GetVisualStudioVersion() != '2017':
327    file_parts = file_parts + ('vcruntime140_1', )
328  for file_part in file_parts:
329    dll = file_part + suffix
330    target = os.path.join(target_dir, dll)
331    source = os.path.join(source_dir, dll)
332    _CopyRuntimeImpl(target, source)
333  # We must copy ucrtbased.dll for all CPU types. The rest of the Universal CRT
334  # is installed as part of the OS in Windows 10 and beyond.
335  if not suffix.startswith('.'):
336    win_sdk_dir = os.path.normpath(
337        os.environ.get(
338            'WINDOWSSDKDIR',
339            os.path.expandvars('%ProgramFiles(x86)%'
340                               '\\Windows Kits\\10')))
341    # ucrtbased.dll is located at {win_sdk_dir}/bin/{a.b.c.d}/{target_cpu}/
342    # ucrt/.
343    sdk_bin_root = os.path.join(win_sdk_dir, 'bin')
344    sdk_bin_sub_dirs = glob.glob(os.path.join(sdk_bin_root, '10.*'))
345    # Select the most recent SDK if there are multiple versions installed.
346    _SortByHighestVersionNumberFirst(sdk_bin_sub_dirs)
347    for directory in sdk_bin_sub_dirs:
348      sdk_redist_root_version = os.path.join(sdk_bin_root, directory)
349      if not os.path.isdir(sdk_redist_root_version):
350        continue
351      source_dir = os.path.join(sdk_redist_root_version, target_cpu, 'ucrt')
352      if not os.path.isdir(source_dir):
353        continue
354      break
355    _CopyRuntimeImpl(os.path.join(target_dir, 'ucrtbase' + suffix),
356                     os.path.join(source_dir, 'ucrtbase' + suffix))
357
358
359def FindVCComponentRoot(component):
360  """Find the most recent Tools or Redist or other directory in an MSVC install.
361  Typical results are {toolchain_root}/VC/{component}/MSVC/{x.y.z}. The {x.y.z}
362  version number part changes frequently so the highest version number found is
363  used.
364  """
365
366  SetEnvironmentAndGetRuntimeDllDirs()
367  assert ('GYP_MSVS_OVERRIDE_PATH' in os.environ)
368  vc_component_msvc_root = os.path.join(os.environ['GYP_MSVS_OVERRIDE_PATH'],
369      'VC', component, 'MSVC')
370  vc_component_msvc_contents = glob.glob(
371      os.path.join(vc_component_msvc_root, '14.*'))
372  # Select the most recent toolchain if there are several.
373  _SortByHighestVersionNumberFirst(vc_component_msvc_contents)
374  for directory in vc_component_msvc_contents:
375    if os.path.isdir(directory):
376      return directory
377  raise Exception('Unable to find the VC %s directory.' % component)
378
379
380def FindVCRedistRoot():
381  """In >=VS2017, Redist binaries are located in
382  {toolchain_root}/VC/Redist/MSVC/{x.y.z}/{target_cpu}/.
383
384  This returns the '{toolchain_root}/VC/Redist/MSVC/{x.y.z}/' path.
385  """
386  return FindVCComponentRoot('Redist')
387
388
389def _CopyRuntime(target_dir, source_dir, target_cpu, debug):
390  """Copy the VS runtime DLLs, only if the target doesn't exist, but the target
391  directory does exist. Handles VS 2015, 2017 and 2019."""
392  suffix = 'd.dll' if debug else '.dll'
393  # VS 2015, 2017 and 2019 use the same CRT DLLs.
394  _CopyUCRTRuntime(target_dir, source_dir, target_cpu, suffix)
395
396
397def CopyDlls(target_dir, configuration, target_cpu):
398  """Copy the VS runtime DLLs into the requested directory as needed.
399
400  configuration is one of 'Debug' or 'Release'.
401  target_cpu is one of 'x86', 'x64' or 'arm64'.
402
403  The debug configuration gets both the debug and release DLLs; the
404  release config only the latter.
405  """
406  vs_runtime_dll_dirs = SetEnvironmentAndGetRuntimeDllDirs()
407  if not vs_runtime_dll_dirs:
408    return
409
410  x64_runtime, x86_runtime, arm64_runtime = vs_runtime_dll_dirs
411  if target_cpu == 'x64':
412    runtime_dir = x64_runtime
413  elif target_cpu == 'x86':
414    runtime_dir = x86_runtime
415  elif target_cpu == 'arm64':
416    runtime_dir = arm64_runtime
417  else:
418    raise Exception('Unknown target_cpu: ' + target_cpu)
419  _CopyRuntime(target_dir, runtime_dir, target_cpu, debug=False)
420  if configuration == 'Debug':
421    _CopyRuntime(target_dir, runtime_dir, target_cpu, debug=True)
422  _CopyDebugger(target_dir, target_cpu)
423  if target_cpu == 'arm64':
424    target_dir = os.path.join(target_dir, 'win_clang_x64')
425    target_cpu = 'x64'
426    runtime_dir = x64_runtime
427    os.makedirs(target_dir, exist_ok=True)
428    _CopyRuntime(target_dir, runtime_dir, target_cpu, debug=False)
429    if configuration == 'Debug':
430      _CopyRuntime(target_dir, runtime_dir, target_cpu, debug=True)
431    _CopyDebugger(target_dir, target_cpu)
432
433
434def _CopyDebugger(target_dir, target_cpu):
435  """Copy dbghelp.dll, dbgcore.dll, and msdia140.dll into the requested
436  directory.
437
438  target_cpu is one of 'x86', 'x64' or 'arm64'.
439
440  dbghelp.dll is used when Chrome needs to symbolize stacks. Copying this file
441  from the SDK directory avoids using the system copy of dbghelp.dll which then
442  ensures compatibility with recent debug information formats, such as
443  large-page PDBs. Note that for these DLLs to be deployed to swarming bots they
444  also need to be listed in group("runtime_libs").
445
446  dbgcore.dll is needed when using some functions from dbghelp.dll (like
447  MinidumpWriteDump).
448
449  msdia140.dll is needed for tools like symupload.exe and dump_syms.exe.
450  """
451  win_sdk_dir = SetEnvironmentAndGetSDKDir()
452  if not win_sdk_dir:
453    return
454
455  # List of debug files that should be copied, the first element of the tuple is
456  # the name of the file and the second indicates if it's optional.
457  debug_files = [('dbghelp.dll', False), ('dbgcore.dll', True)]
458  for debug_file, is_optional in debug_files:
459    full_path = os.path.join(win_sdk_dir, 'Debuggers', target_cpu, debug_file)
460    if not os.path.exists(full_path):
461      if is_optional:
462        continue
463      else:
464        raise Exception('%s not found in "%s"\r\nYou must install '
465                        'Windows 10 SDK version %s including the '
466                        '"Debugging Tools for Windows" feature.' %
467                        (debug_file, full_path, SDK_VERSION))
468    target_path = os.path.join(target_dir, debug_file)
469    _CopyRuntimeImpl(target_path, full_path)
470
471  # The x64 version of msdia140.dll is always used because symupload and
472  # dump_syms are always built as x64 binaries.
473  dia_path = os.path.join(NormalizePath(os.environ['GYP_MSVS_OVERRIDE_PATH']),
474                          'DIA SDK', 'bin', 'amd64', 'msdia140.dll')
475  _CopyRuntimeImpl(os.path.join(target_dir, 'msdia140.dll'), dia_path)
476
477
478def _GetDesiredVsToolchainHashes():
479  """Load a list of SHA1s corresponding to the toolchains that we want installed
480  to build with."""
481  # Third parties that do not have access to the canonical toolchain can map
482  # canonical toolchain version to their own toolchain versions.
483  toolchain_hash_mapping_key = 'GYP_MSVS_HASH_%s' % TOOLCHAIN_HASH
484  return [os.environ.get(toolchain_hash_mapping_key, TOOLCHAIN_HASH)]
485
486
487def ShouldUpdateToolchain():
488  """Check if the toolchain should be upgraded."""
489  if not os.path.exists(json_data_file):
490    return True
491  with open(json_data_file, 'r') as tempf:
492    toolchain_data = json.load(tempf)
493  version = toolchain_data['version']
494  env_version = GetVisualStudioVersion()
495  # If there's a mismatch between the version set in the environment and the one
496  # in the json file then the toolchain should be updated.
497  return version != env_version
498
499
500def Update(force=False, no_download=False):
501  """Requests an update of the toolchain to the specific hashes we have at
502  this revision. The update outputs a .json of the various configuration
503  information required to pass to gyp which we use in |GetToolchainDir()|.
504  If no_download is true then the toolchain will be configured if present but
505  will not be downloaded.
506  """
507  if force != False and force != '--force':
508    print('Unknown parameter "%s"' % force, file=sys.stderr)
509    return 1
510  if force == '--force' or os.path.exists(json_data_file):
511    force = True
512
513  depot_tools_win_toolchain = \
514      bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1')))
515  if (_HostIsWindows() or force) and depot_tools_win_toolchain:
516    import find_depot_tools
517    depot_tools_path = find_depot_tools.add_depot_tools_to_path()
518
519    # On Linux, the file system is usually case-sensitive while the Windows
520    # SDK only works on case-insensitive file systems.  If it doesn't already
521    # exist, set up a ciopfs fuse mount to put the SDK in a case-insensitive
522    # part of the file system.
523    toolchain_dir = os.path.join(depot_tools_path, 'win_toolchain', 'vs_files')
524    # For testing this block, unmount existing mounts with
525    # fusermount -u third_party/depot_tools/win_toolchain/vs_files
526    if sys.platform.startswith('linux') and not os.path.ismount(toolchain_dir):
527      ciopfs = shutil.which('ciopfs')
528      if not ciopfs:
529        # ciopfs not found in PATH; try the one downloaded from the DEPS hook.
530        ciopfs = os.path.join(script_dir, 'ciopfs')
531      if not os.path.isdir(toolchain_dir):
532        os.mkdir(toolchain_dir)
533      if not os.path.isdir(toolchain_dir + '.ciopfs'):
534        os.mkdir(toolchain_dir + '.ciopfs')
535      # Without use_ino, clang's #pragma once and Wnonportable-include-path
536      # both don't work right, see https://llvm.org/PR34931
537      # use_ino doesn't slow down builds, so it seems there's no drawback to
538      # just using it always.
539      subprocess.check_call([
540          ciopfs, '-o', 'use_ino', toolchain_dir + '.ciopfs', toolchain_dir])
541
542    get_toolchain_args = [
543        sys.executable,
544        os.path.join(depot_tools_path,
545                    'win_toolchain',
546                    'get_toolchain_if_necessary.py'),
547        '--output-json', json_data_file,
548      ] + _GetDesiredVsToolchainHashes()
549    if force:
550      get_toolchain_args.append('--force')
551    if no_download:
552      get_toolchain_args.append('--no-download')
553    subprocess.check_call(get_toolchain_args)
554
555  return 0
556
557
558def NormalizePath(path):
559  while path.endswith('\\'):
560    path = path[:-1]
561  return path
562
563
564def SetEnvironmentAndGetSDKDir():
565  """Gets location information about the current sdk (must have been
566  previously updated by 'update'). This is used for the GN build."""
567  SetEnvironmentAndGetRuntimeDllDirs()
568
569  # If WINDOWSSDKDIR is not set, search the default SDK path and set it.
570  if not 'WINDOWSSDKDIR' in os.environ:
571    default_sdk_path = os.path.expandvars('%ProgramFiles(x86)%'
572                                          '\\Windows Kits\\10')
573    if os.path.isdir(default_sdk_path):
574      os.environ['WINDOWSSDKDIR'] = default_sdk_path
575
576  return NormalizePath(os.environ['WINDOWSSDKDIR'])
577
578
579def SDKIncludesIDCompositionDevice4():
580  """Returns true if the selected Windows SDK includes the declaration for the
581    IDCompositionDevice4 interface. This is essentially the equivalent checking
582    if a (non-preview) SDK version >=10.0.22621.2428.
583
584    We cannot check for this SDK version directly since it installs to a folder
585    with the minor version set to 0 (i.e. 10.0.22621.0) and the
586    IDCompositionDevice4 interface was added in a servicing release which did
587    not increment the major version.
588
589    There doesn't seem to be a straightforward and cross-platform way to get the
590    minor version of an installed SDK directory. To work around this, we look
591    for the GUID declaring the interface which implies the SDK version and
592    ensures the interface itself is present."""
593  win_sdk_dir = SetEnvironmentAndGetSDKDir()
594  if not win_sdk_dir:
595    return False
596
597  # Skip this check if we know the major version definitely includes
598  # IDCompositionDevice4.
599  if int(SDK_VERSION.split('.')[2]) > 22621:
600    return True
601
602  dcomp_header_path = os.path.join(win_sdk_dir, 'Include', SDK_VERSION, 'um',
603                                   'dcomp.h')
604  DECLARE_DEVICE4_LINE = ('DECLARE_INTERFACE_IID_('
605                          'IDCompositionDevice4, IDCompositionDevice3, '
606                          '"85FC5CCA-2DA6-494C-86B6-4A775C049B8A")')
607  with open(dcomp_header_path) as f:
608    for line in f.readlines():
609      if line.rstrip() == DECLARE_DEVICE4_LINE:
610        return True
611
612  return False
613
614
615def GetToolchainDir():
616  """Gets location information about the current toolchain (must have been
617  previously updated by 'update'). This is used for the GN build."""
618  runtime_dll_dirs = SetEnvironmentAndGetRuntimeDllDirs()
619  win_sdk_dir = SetEnvironmentAndGetSDKDir()
620  version_as_year = GetVisualStudioVersion()
621
622  if not SDKIncludesIDCompositionDevice4():
623    print(
624        'Windows SDK >= 10.0.22621.2428 required. You can get it by updating '
625        f'Visual Studio {version_as_year} using the Visual Studio Installer.',
626        file=sys.stderr,
627    )
628    return 1
629
630  print('''vs_path = %s
631sdk_version = %s
632sdk_path = %s
633vs_version = %s
634wdk_dir = %s
635runtime_dirs = %s
636''' % (ToGNString(NormalizePath(
637      os.environ['GYP_MSVS_OVERRIDE_PATH'])), ToGNString(SDK_VERSION),
638       ToGNString(win_sdk_dir), ToGNString(version_as_year),
639       ToGNString(NormalizePath(os.environ.get('WDK_DIR', ''))),
640       ToGNString(os.path.pathsep.join(runtime_dll_dirs or ['None']))))
641
642
643def main():
644  commands = {
645      'update': Update,
646      'get_toolchain_dir': GetToolchainDir,
647      'copy_dlls': CopyDlls,
648  }
649  if len(sys.argv) < 2 or sys.argv[1] not in commands:
650    print('Expected one of: %s' % ', '.join(commands), file=sys.stderr)
651    return 1
652  return commands[sys.argv[1]](*sys.argv[2:])
653
654
655if __name__ == '__main__':
656  sys.exit(main())
657