Repository: SCons/scons (language: python)

Issue:
[WIP] MSVC: detection fixes and changes
Changes:
* VS/VC roots are classified by their installed features (e.g, `devenv.com`, `vcexpress.com`, etc.). This provides for special-case verification of batch file arguments based on the internal classification.
* Consistent with earlier behavior, express versions are used for the non-express msvc version symbol for `14.1` and `8.0` when the express version is the only version installed.
* There is a strong possibility that `14.0Exp` may not have been detected correctly.  It appears that registry keys for earlier versions of msvc are not populated.  Refined detection of `14.0Exp` was added.
* Special case handling of VS2015 BuildTools was added.  The msvc batch files restrict the arguments available as compared to full versions.  The arguments may be accepted and ignored possibly resulting in build failures that are likely not easy to diagnose.
* Special case handling of VS2015 Express was added.  The msvc batch files restrict the arguments available as compared to full versions.  For example, store/UWP build are only available for `x86` targets.
* Windows/Platform SDK installations of `7.1`, `7.0`, and `6.1` populate registry keys and installation folders that were detected by scons (versions `10.0` and `9.0`).  Unfortunately, the generated files are intended to be used via `SetEnv.cmd` and result in errors.  The detection of sdk-only installations was added and the roots are ignored.
* The relative imports of the MSCommon module were changed to top-level absolute imports in a number of microsoft tools.  Moving any of the tools to the site tools folder failed on import (i.e., the relative paths become invalid when moved).
* VS2005 to VS2015 batch file (`vcvarsall.bat`) dispatches to a dependent batch file when configuring the msvc environment.  In certain installation scenarios, the dependent batch file (e.g., `vcvars64.bat`) may not exist.  The existence of `vcvarsall.bat`, the dependent batch file, and the compiler executable are now verified.
* MSVC configuration data specific to versions VS2005 to VS2008 was added as the dependent batch files have different names than the batch files for VS2010 and later.  VC++ For Python is handled as a special case as the dependent batch files: are not used and are in different locations.
* When VC++ For Python is installed using the `ALLUSERS=1` command-line option, the registry keys written are under `HKLM` rather than `HKCU`.  VC++ For Python installed for all users is now correctly detected.
* The existing detection configuration for vswhere and the registry was refactored to separate the two methods of detection.
* The detection of the msvc compiler executable has been modified and no longer considers the os environment.  The detection of the msvc compiler executable was modified to provide more detailed warning messages.

Internal code changes:
* Rename VISUALSTUDIO_ to MSVS_PRODUCT and vs_def to vs_product_def in all files.
* Replace process_path with normalize_path with additional arguments in MSCommon.MSVC.Util.
* Add msvs channel, msvs component id, and msvs component definitions.
* Add Validate module to MSCommon.MSVC.
* Add AutoInitialize class to MSCommon for object initialization upon class definition completion.
* Add custom formatter to MSCommon debug logging to prepend the class name to the function name in the log record based on passed arguments.
* Rework MSCommon.Kind to return the msvs component, vs directory, vs executable, and vc feature map.
* Combine _VCVER_TO_PRODUCT_DIR vc version keys into vs product keys and use MSCommon.MSVC.Kind to determine the vs component type.
* Add file exists and normalized path caches to MSCommon.vc.
* Rework vswhere executable management and queries.
* Add an object hierarchy for vs/vc detection. Include preliminary data structure construction for msvs channels (preview, release, any) and component types (Enterprise, Professional, Community, BuildTools, Express, etc).
* Select an msvc instance rather than a vc dir and pass the instance to MSCommon.MSVC.ScriptArgument methods.
* Rework msvc action selection (use script, select, use settings, bypass).

## Contributor Checklist:

* [x] I have created a new test or updated the unit tests to cover the new/changed functionality.
* [x] I have updated `CHANGES.txt` (and read the `README.rst`)
* [x] I have updated the appropriate documentation

[WIP] MSVC: detection fixes and changes
Changes:
* VS/VC roots are classified by their installed features (e.g, `devenv.com`, `vcexpress.com`, etc.). This provides for special-case verification of batch file arguments based on the internal classification.
* Consistent with earlier behavior, express versions are used for the non-express msvc version symbol for `14.1` and `8.0` when the express version is the only version installed.
* There is a strong possibility that `14.0Exp` may not have been detected correctly.  It appears that registry keys for earlier versions of msvc are not populated.  Refined detection of `14.0Exp` was added.
* Special case handling of VS2015 BuildTools was added.  The msvc batch files restrict the arguments available as compared to full versions.  The arguments may be accepted and ignored possibly resulting in build failures that are likely not easy to diagnose.
* Special case handling of VS2015 Express was added.  The msvc batch files restrict the arguments available as compared to full versions.  For example, store/UWP build are only available for `x86` targets.
* Windows/Platform SDK installations of `7.1`, `7.0`, and `6.1` populate registry keys and installation folders that were detected by scons (versions `10.0` and `9.0`).  Unfortunately, the generated files are intended to be used via `SetEnv.cmd` and result in errors.  The detection of sdk-only installations was added and the roots are ignored.
* The relative imports of the MSCommon module were changed to top-level absolute imports in a number of microsoft tools.  Moving any of the tools to the site tools folder failed on import (i.e., the relative paths become invalid when moved).
* VS2005 to VS2015 batch file (`vcvarsall.bat`) dispatches to a dependent batch file when configuring the msvc environment.  In certain installation scenarios, the dependent batch file (e.g., `vcvars64.bat`) may not exist.  The existence of `vcvarsall.bat`, the dependent batch file, and the compiler executable are now verified.
* MSVC configuration data specific to versions VS2005 to VS2008 was added as the dependent batch files have different names than the batch files for VS2010 and later.  VC++ For Python is handled as a special case as the dependent batch files: are not used and are in different locations.
* When VC++ For Python is installed using the `ALLUSERS=1` command-line option, the registry keys written are under `HKLM` rather than `HKCU`.  VC++ For Python installed for all users is now correctly detected.
* The existing detection configuration for vswhere and the registry was refactored to separate the two methods of detection.
* The detection of the msvc compiler executable has been modified and no longer considers the os environment.  The detection of the msvc compiler executable was modified to provide more detailed warning messages.

Internal code changes:
* Rename VISUALSTUDIO_ to MSVS_PRODUCT and vs_def to vs_product_def in all files.
* Replace process_path with normalize_path with additional arguments in MSCommon.MSVC.Util.
* Add msvs channel, msvs component id, and msvs component definitions.
* Add Validate module to MSCommon.MSVC.
* Add AutoInitialize class to MSCommon for object initialization upon class definition completion.
* Add custom formatter to MSCommon debug logging to prepend the class name to the function name in the log record based on passed arguments.
* Rework MSCommon.Kind to return the msvs component, vs directory, vs executable, and vc feature map.
* Combine _VCVER_TO_PRODUCT_DIR vc version keys into vs product keys and use MSCommon.MSVC.Kind to determine the vs component type.
* Add file exists and normalized path caches to MSCommon.vc.
* Rework vswhere executable management and queries.
* Add an object hierarchy for vs/vc detection. Include preliminary data structure construction for msvs channels (preview, release, any) and component types (Enterprise, Professional, Community, BuildTools, Express, etc).
* Select an msvc instance rather than a vc dir and pass the instance to MSCommon.MSVC.ScriptArgument methods.
* Rework msvc action selection (use script, select, use settings, bypass).

## Contributor Checklist:

* [x] I have created a new test or updated the unit tests to cover the new/changed functionality.
* [x] I have updated `CHANGES.txt` (and read the `README.rst`)
* [x] I have updated the appropriate documentation

Microsoft toolset 14.40 does not work with SCons 4.7 and earlier
This was an anticipated issue (see Discussion #4484) but now it has landed (late May 2024) and indeed things break.

Visual Studio 17.10 has released now, and with it a toolset numbered 14.40. As noted in the linked discussion, previously a version like 14.40 would have indicated the v144 build tools, but this is not the case, we're still on v143.  Microsoft knows this could cause some breakage and there's a blog post about it in the discussion, but here we are: things are broken.

These tests failed in a full run:
```
Failed the following 7 tests:
        SCons\Tool\MSCommon\MSVC\ScriptArgumentsTests.py
        SCons\Tool\MSCommon\vcTests.py
        test\MSVC\msvc_cache_force_defaults.py
        test\MSVC\MSVC_SDK_VERSION.py
        test\MSVC\MSVC_SPECTRE_LIBS.py
        test\MSVC\MSVC_TOOLSET_VERSION.py
        test\MSVC\MSVC_UWP_APP.py
```
Errors seen include the very expected:
```
Traceback (most recent call last):
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArgumentsTests.py", line 573, in test_msvc_script_args_none
    self.run_msvc_script_args_none()
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArgumentsTests.py", line 292, in run_msvc_script_args_none
    _ = func(env, version_def.msvc_version, vc_dir, '')
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArguments.py", line 929, in msvc_script_arguments
    toolset = _toolset_version(toolset_version)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArguments.py", line 190, in _toolset_version
    vs_def = Config.MSVC_VERSION_INTERNAL[verstr]
             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^
KeyError: '14.4'
```
as well as
```
Traceback (most recent call last):
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArgumentsTests.py", line 578, in test_msvc_script_args
    self.run_msvc_script_args()
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArgumentsTests.py", line 310, in run_msvc_script_args
    earlier_toolset_versions = [toolset_def for toolset_def in toolset_versions if toolset_def.msvc_vernum != version_def.msvc_vernum]
                                                                                                              ^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'msvc_vernum'
````

Microsoft toolset 14.40 does not work with SCons 4.7 and earlier
This was an anticipated issue (see Discussion #4484) but now it has landed (late May 2024) and indeed things break.

Visual Studio 17.10 has released now, and with it a toolset numbered 14.40. As noted in the linked discussion, previously a version like 14.40 would have indicated the v144 build tools, but this is not the case, we're still on v143.  Microsoft knows this could cause some breakage and there's a blog post about it in the discussion, but here we are: things are broken.

These tests failed in a full run:
```
Failed the following 7 tests:
        SCons\Tool\MSCommon\MSVC\ScriptArgumentsTests.py
        SCons\Tool\MSCommon\vcTests.py
        test\MSVC\msvc_cache_force_defaults.py
        test\MSVC\MSVC_SDK_VERSION.py
        test\MSVC\MSVC_SPECTRE_LIBS.py
        test\MSVC\MSVC_TOOLSET_VERSION.py
        test\MSVC\MSVC_UWP_APP.py
```
Errors seen include the very expected:
```
Traceback (most recent call last):
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArgumentsTests.py", line 573, in test_msvc_script_args_none
    self.run_msvc_script_args_none()
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArgumentsTests.py", line 292, in run_msvc_script_args_none
    _ = func(env, version_def.msvc_version, vc_dir, '')
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArguments.py", line 929, in msvc_script_arguments
    toolset = _toolset_version(toolset_version)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArguments.py", line 190, in _toolset_version
    vs_def = Config.MSVC_VERSION_INTERNAL[verstr]
             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^
KeyError: '14.4'
```
as well as
```
Traceback (most recent call last):
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArgumentsTests.py", line 578, in test_msvc_script_args
    self.run_msvc_script_args()
  File "C:\Users\mats\Documents\github\scons\SCons\Tool\MSCommon\MSVC\ScriptArgumentsTests.py", line 310, in run_msvc_script_args
    earlier_toolset_versions = [toolset_def for toolset_def in toolset_versions if toolset_def.msvc_vernum != version_def.msvc_vernum]
                                                                                                              ^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'msvc_vernum'
````



Discussion / hints:

Fix patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index e2fe339db1..9d69e707ad 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -12,6 +12,64 @@ NOTE: Python 3.6 support is deprecated and will be dropped in a future reease.
 
 RELEASE  VERSION/DATE TO BE FILLED IN LATER
 
+  From Joseph Brill:
+    - For msvc version specifications without an 'Exp' suffix, an express installation
+      is used when no other edition is detected for the msvc version.  Similarly, an
+      express installation of the IDE binary is used when no other IDE edition is
+      detected.
+    - VS2015 Express (14.0Exp) does not support the sdk version argument.  VS2015 Express
+      does not support the store argument for target architectures other than x86.
+      Script argument validation now takes into account these restrictions.
+    - VS2015 BuildTools (14.0) does not support the sdk version argument and does not
+      support the store argument.  Script argument validation now takes into account
+      these restrictions.
+    - The Windows SDK for Windows 7 and .NET Framework 4" (SDK 7.1) populates the
+      registry keys in a manner in which the msvc detection would report that VS2010
+      (10.0) is installed when only the SDK was installed.  The installed files are
+      intended to be used via the sdk batch file setenv.cmd. The installed msvc
+      batch files will fail. The msvc detection logic now ignores SDK-only VS2010
+      installations.  Similar protection is implemented for the sdk-only installs that
+      populate the installation folder and registry keys for VS2008 (9.0), if necessary.
+    - For VS2005 (8.0) to VS2015 (14.0), vsvarsall.bat is employed to dispatch to a
+      dependent batch file when configuring the msvc environment.  Previously, only the
+      existence of the compiler executable was verified. In certain installations, the
+      dependent batch file (e.g., vcvars64.bat) may not exist while the compiler
+      executable does exist resulting in build failures.  The existence of vcvarsall.bat,
+      the dependent batch file, and the compiler executable are now validated.
+    - MSVC configuration data specific to versions VS2005 (8.0) to VS2008 (9.0) was added
+      as the dependent batch files have different names than the batch file names used
+      for VS2010 (10.0) and later.  The VS2008 (9.0) Visual C++ For Python installation
+      is handled as a special case as the dependent batch files are: (a) not used and (b)
+      in different locations.
+    - When VS2008 (9.0) Visual C++ For Python is installed using the ALLUSERS=1 option
+      (i.e., msiexec /i VCForPython27.msi ALLUSERS=1), the registry keys are written to
+      HKEY_LOCAL_MACHINE rather than HKEY_CURRENT_USER.  An entry was added to query the
+      Visual C++ For Python keys in HKLM following the HKCU query, if necessary.
+    - For VS2008, a full development edition (e.g., Professional) is now selected before
+      a Visual C++ For Python edition.  Prior to this change, Visual C++ For Python was
+      selected before a full development edition when both editions are installed.
+    - The registry detection of VS2015 (14.0), and earlier, is now cached at runtime and
+      is only evaluated once for each msvc version.
+    - The vswhere executable is frozen upon initial detection.  Specifying a different
+      vswhere executable via the construction variable VSWHERE after the initial
+      detection now results in an exception.  Multiple bugs in the implementation of
+      specifying a vswhere executable via the construction variable VSWHERE have been
+      fixed.  Previously, when a user specified vswhere executable detects new msvc
+      installations after the initial detection, the internal msvc installation cache
+      and the default msvc version based on the initial detection are no longer valid.
+      For example, when no vswhere executable is found for the initial detection
+      and then later an environment is constructed with a user specified vswhere
+      executable that detects new msvc installations.
+    - The vswhere detection of VS2017 (14.1), and later, is now cached at runtime and is
+      only evaluated once using a single vswhere invocation for all msvc versions.
+      Previously, the vswhere executable was invoked for each supported msvc version.
+    - The vswhere executable locations for the WinGet and Scoop package managers were
+      added to the default vswhere executable search list after the Chocolatey
+      installation location.
+    - Fix issue #4543: add support for msvc toolset versions 14.4X installed as the
+      latest msvc toolset versions for msvc buildtools v143.  The v143 msvc buildtools
+      may contain msvc toolset versions from 14.30 to 14.4X.
+
   From Thaddeus Crews:
     - GetSConsVersion() to grab the latest SCons version without needing to
       access SCons internals.
@@ -1490,7 +1548,7 @@ RELEASE 3.1.2 - Mon, 17 Dec 2019 02:06:27 +0000
       in the case where a child process is spawned while a Python action has a
       file open. Original author: Ryan Beasley.
     - Added memoization support for calls to Environment.Value() in order to
-	  improve performance of repeated calls.
+      improve performance of repeated calls.
 
 
   From Jason Kenny
diff --git a/RELEASE.txt b/RELEASE.txt
index 8851109e06..aa17f64839 100644
--- a/RELEASE.txt
+++ b/RELEASE.txt
@@ -44,6 +44,15 @@ CHANGED/ENHANCED EXISTING FUNCTIONALITY
   of CCFLAGS; the latter variable could cause a compiler warning.
 - The implementation of Variables was slightly refactored, there should
   not be user-visible changes.
+- MSVC: For msvc version specifications without an 'Exp' suffix, an express
+  installation is used when no other edition is detected for the msvc version.
+  This was the behavior for Visual Studio 2008 (9.0) through Visual Studio 2015
+  (14.0).  This behavior was extended to Visual Studio 2017 (14.1) and Visual
+  Studio 2008 (8.0).  An express installation of the IDE binary is used when no
+  other IDE edition is detected.
+- The vswhere executable locations for the WinGet and Scoop package managers were
+  added to the default vswhere executable search list after the Chocolatey
+  installation location.
 - SCons.Environment.is_valid_construction_var() now returns a boolean to
   match the convention that functions beginning with "is" have yes/no
   answers (previously returned either None or an re.match object).
@@ -73,6 +82,37 @@ FIXES
 - Improved the conversion of a "foreign" exception from an action
   into BuildError by making sure our defaults get applied even in
   corner cases. Fixes Issue #4530
+- MSVC: Visual Studio 2010 (10.0) could be inadvertently detected due to an
+  sdk-only install of Windows SDK 7.1.  An sdk-only install of Visual Studio
+  2010 is ignored as the msvc batch files will fail. The installed files are
+  intended to be used in conjunction with the SDK batch file.  Similar protection
+  was added for Visual Studio 2008 (9.0).
+- MSVC: For Visual Studio 2005 (8.0) to Visual Studio 2015 (14.0), detection of
+  installed files was expanded to include the primary msvc batch file, dependent
+  msvc batch file, and compiler executable. In certain installations, the
+  dependent msvc batch file may not exist while the compiler executable does exist
+  resulting in a build failure.
+- MSVC: Visual Studio 2008 (9.0) Visual C++ For Python was not detected when
+  installed using the ALLUSERS command-line option:
+      msiexec /i VCForPython27.msi ALLUSERS=1
+  When installed for all users, Visual Studio 2008 (9.0) Visual C++ For Python is
+  now correctly detected.
+- MSVC: For Visual Studio 2008 (9.0), a full development edition (e.g., Professional)
+  is now selected before a Visual C++ For Python edition. Prior to this change,
+  Visual C++ For Python was selected before a full development edition when both
+  editions are installed.
+- The vswhere executable is frozen upon initial detection.  Specifying a different
+  vswhere executable via the construction variable VSWHERE after the initial
+  detection now results in an exception.  Multiple bugs in the implementation of
+  specifying a vswhere executable via the construction variable VSWHERE have been
+  fixed.  Previously, when a user specified vswhere executable detects new msvc
+  installations after the initial detection, the internal msvc installation cache
+  and the default msvc version based on the initial detection are no longer valid.
+  For example, when no vswhere executable is found for the initial detection
+  and then later an environment is constructed with a user specified vswhere
+  executable that detects new msvc installations.
+- MSVC: Visual Studio 2022 v143 BuildTools now supports msvc toolset versions from
+  14.30 to 14.4X.  Fixes Issue #4543.
 - The Clone() method now respects the variables argument (fixes #3590)
 
 IMPROVEMENTS
@@ -84,6 +124,17 @@ IMPROVEMENTS
 - Performance tweak: the __setitem__ method of an Environment, used for
   setting construction variables, now uses the string method isidentifier
   to validate the name (updated from microbenchmark results).
+- MSVC: Visual Studio 2015 Express (14.0Exp) does not support the sdk version
+  argument. Visual Studio 2015 Express does not support the store argument for
+  target architectures other than x86.  Script argument validation now takes into
+  account these restrictions.
+- MSVC: Visual Studio 2015 BuildTools (14.0) does not support the sdk version
+  argument and does not support the store argument.  Script argument validation now
+  takes into account these restrictions.
+- MSVC: The registry detection of VS2015 (14.0), and earlier, is now cached at runtime
+  and is only evaluated once for each msvc version.
+- MSVC: The vswhere detection of VS2017 (14.1), and later, is now cached at runtime and
+  is only evaluated once using a single vswhere invocation for all msvc versions.
 
 PACKAGING
 ---------
diff --git a/SCons/Tool/MSCommon/MSVC/Config.py b/SCons/Tool/MSCommon/MSVC/Config.py
index 7c0f1fe6ff..d0ccb6d0ba 100644
--- a/SCons/Tool/MSCommon/MSVC/Config.py
+++ b/SCons/Tool/MSCommon/MSVC/Config.py
@@ -118,15 +118,67 @@
     for vc_runtime_alias in vc_runtime_alias_list:
         MSVC_RUNTIME_EXTERNAL[vc_runtime_alias] = vc_runtime_def
 
-MSVC_BUILDTOOLS_DEFINITION = namedtuple('MSVCBuildtools', [
-    'vc_buildtools',
-    'vc_buildtools_numeric',
+MSVC_BUILDSERIES_DEFINITION = namedtuple('MSVCBuildSeries', [
+    'vc_buildseries',
+    'vc_buildseries_numeric',
     'vc_version',
     'vc_version_numeric',
     'cl_version',
     'cl_version_numeric',
+])
+
+MSVC_BUILDSERIES_DEFINITION_LIST = []
+
+MSVC_BUILDSERIES_INTERNAL = {}
+MSVC_BUILDSERIES_EXTERNAL = {}
+
+VC_BUILDTOOLS_MAP = {}
+
+VC_VERSION_MAP = {}
+CL_VERSION_MAP = {}
+
+for (vc_buildseries, vc_version, cl_version) in [
+    ('144', '14.4', '19.4'),
+    ('143', '14.3', '19.3'),
+    ('142', '14.2', '19.2'),
+    ('141', '14.1', '19.1'),
+    ('140', '14.0', '19.0'),
+    ('120', '12.0', '18.0'),
+    ('110', '11.0', '17.0'),
+    ('100', '10.0', '16.0'),
+    ('90', '9.0', '15.0'),
+    ('80', '8.0', '14.0'),
+    ('71', '7.1', '13.1'),
+    ('70', '7.0', '13.0'),
+    ('60', '6.0', '12.0'),
+]:
+
+    vc_buildseries_def = MSVC_BUILDSERIES_DEFINITION(
+        vc_buildseries=vc_buildseries,
+        vc_buildseries_numeric=int(vc_buildseries),
+        vc_version=vc_version,
+        vc_version_numeric=float(vc_version),
+        cl_version=cl_version,
+        cl_version_numeric=float(cl_version),
+    )
+
+    MSVC_BUILDSERIES_DEFINITION_LIST.append(vc_buildseries_def)
+
+    MSVC_BUILDSERIES_INTERNAL[vc_buildseries] = vc_buildseries_def
+    MSVC_BUILDSERIES_EXTERNAL[vc_buildseries] = vc_buildseries_def
+    MSVC_BUILDSERIES_EXTERNAL[vc_version] = vc_buildseries_def
+
+    VC_VERSION_MAP[vc_version] = vc_buildseries_def
+    CL_VERSION_MAP[cl_version] = vc_buildseries_def
+
+MSVC_BUILDTOOLS_DEFINITION = namedtuple('MSVCBuildtools', [
+    'vc_buildtools',
+    'vc_buildtools_numeric',
+    'vc_buildseries_list',
     'vc_runtime_def',
     'vc_istoolset',
+    'msvc_version',
+    'msvc_version_numeric',
 ])
 
 MSVC_BUILDTOOLS_DEFINITION_LIST = []
@@ -134,43 +186,58 @@
 MSVC_BUILDTOOLS_INTERNAL = {}
 MSVC_BUILDTOOLS_EXTERNAL = {}
 
-VC_VERSION_MAP = {}
-
-for vc_buildtools, vc_version, cl_version, vc_runtime, vc_istoolset in [
-    ('v143', '14.3', '19.3', '140', True),
-    ('v142', '14.2', '19.2', '140', True),
-    ('v141', '14.1', '19.1', '140', True),
-    ('v140', '14.0', '19.0', '140', True),
-    ('v120', '12.0', '18.0', '120', False),
-    ('v110', '11.0', '17.0', '110', False),
-    ('v100', '10.0', '16.0', '100', False),
-    ('v90',   '9.0', '15.0',  '90', False),
-    ('v80',   '8.0', '14.0',  '80', False),
-    ('v71',   '7.1', '13.1',  '71', False),
-    ('v70',   '7.0', '13.0',  '70', False),
-    ('v60',   '6.0', '12.0',  '60', False),
+MSVC_VERSION_NEWEST = None
+MSVC_VERSION_NEWEST_NUMERIC = 0.0
+
+for vc_buildtools, vc_buildseries_list, vc_runtime, vc_istoolset in [
+    ('v143', ['144', '143'], '140', True),
+    ('v142', ['142'], '140', True),
+    ('v141', ['141'], '140', True),
+    ('v140', ['140'], '140', True),
+    ('v120', ['120'], '120', False),
+    ('v110', ['110'], '110', False),
+    ('v100', ['100'], '100', False),
+    ('v90', ['90'], '90', False),
+    ('v80', ['80'], '80', False),
+    ('v71', ['71'], '71', False),
+    ('v70', ['70'], '70', False),
+    ('v60', ['60'], '60', False),
 ]:
 
     vc_runtime_def = MSVC_RUNTIME_INTERNAL[vc_runtime]
 
+    vc_buildseries_list = tuple(
+        MSVC_BUILDSERIES_INTERNAL[vc_buildseries]
+        for vc_buildseries in vc_buildseries_list
+    )
+
+    vc_buildtools_numstr = vc_buildtools[1:]
+
+    msvc_version = vc_buildtools_numstr[:-1] + '.' + vc_buildtools_numstr[-1]
+    msvc_version_numeric = float(msvc_version)
+
     vc_buildtools_def = MSVC_BUILDTOOLS_DEFINITION(
         vc_buildtools = vc_buildtools,
         vc_buildtools_numeric = int(vc_buildtools[1:]),
-        vc_version = vc_version,
-        vc_version_numeric = float(vc_version),
-        cl_version = cl_version,
-        cl_version_numeric = float(cl_version),
+        vc_buildseries_list = vc_buildseries_list,
         vc_runtime_def = vc_runtime_def,
         vc_istoolset = vc_istoolset,
+        msvc_version = msvc_version,
+        msvc_version_numeric = msvc_version_numeric,
     )
 
     MSVC_BUILDTOOLS_DEFINITION_LIST.append(vc_buildtools_def)
 
     MSVC_BUILDTOOLS_INTERNAL[vc_buildtools] = vc_buildtools_def
     MSVC_BUILDTOOLS_EXTERNAL[vc_buildtools] = vc_buildtools_def
-    MSVC_BUILDTOOLS_EXTERNAL[vc_version] = vc_buildtools_def
+    MSVC_BUILDTOOLS_EXTERNAL[msvc_version] = vc_buildtools_def
+
+    for vc_buildseries_def in vc_buildseries_list:
+        VC_BUILDTOOLS_MAP[vc_buildseries_def.vc_buildseries] = vc_buildtools_def
 
-    VC_VERSION_MAP[vc_version] = vc_buildtools_def
+    if vc_buildtools_def.msvc_version_numeric > MSVC_VERSION_NEWEST_NUMERIC:
+        MSVC_VERSION_NEWEST_NUMERIC = vc_buildtools_def.msvc_version_numeric
+        MSVC_VERSION_NEWEST = vc_buildtools_def.msvc_version
 
 MSVS_VERSION_INTERNAL = {}
 MSVS_VERSION_EXTERNAL = {}
@@ -181,8 +248,6 @@
 
 MSVS_VERSION_MAJOR_MAP = {}
 
-CL_VERSION_MAP = {}
-
 MSVC_SDK_VERSIONS = set()
 
 VISUALSTUDIO_DEFINITION = namedtuple('VisualStudioDefinition', [
@@ -247,15 +312,15 @@
 
     vc_buildtools_def.vc_runtime_def.vc_runtime_vsdef_list.append(vs_def)
 
-    vc_version = vc_buildtools_def.vc_version
+    msvc_version = vc_buildtools_def.msvc_version
 
     MSVS_VERSION_INTERNAL[vs_product] = vs_def
     MSVS_VERSION_EXTERNAL[vs_product] = vs_def
     MSVS_VERSION_EXTERNAL[vs_version] = vs_def
 
-    MSVC_VERSION_INTERNAL[vc_version] = vs_def
+    MSVC_VERSION_INTERNAL[msvc_version] = vs_def
     MSVC_VERSION_EXTERNAL[vs_product] = vs_def
-    MSVC_VERSION_EXTERNAL[vc_version] = vs_def
+    MSVC_VERSION_EXTERNAL[msvc_version] = vs_def
     MSVC_VERSION_EXTERNAL[vc_buildtools_def.vc_buildtools] = vs_def
 
     if vs_product in VS_PRODUCT_ALIAS:
@@ -264,14 +329,12 @@
             MSVS_VERSION_EXTERNAL[vs_product_alias] = vs_def
             MSVC_VERSION_EXTERNAL[vs_product_alias] = vs_def
 
-    MSVC_VERSION_SUFFIX[vc_version] = vs_def
+    MSVC_VERSION_SUFFIX[msvc_version] = vs_def
     if vs_express:
-        MSVC_VERSION_SUFFIX[vc_version + 'Exp'] = vs_def
+        MSVC_VERSION_SUFFIX[msvc_version + 'Exp'] = vs_def
 
     MSVS_VERSION_MAJOR_MAP[vs_version_major] = vs_def
 
-    CL_VERSION_MAP[vc_buildtools_def.cl_version] = vs_def
-
     if vc_sdk:
         MSVC_SDK_VERSIONS.update(vc_sdk)
 
@@ -292,7 +355,7 @@
 for vs_def in VISUALSTUDIO_DEFINITION_LIST:
     if not vs_def.vc_buildtools_def.vc_istoolset:
         continue
-    version_key = vs_def.vc_buildtools_def.vc_version
+    version_key = vs_def.vc_buildtools_def.msvc_version
     MSVC_VERSION_TOOLSET_DEFAULTS_MAP[version_key] = [version_key]
     MSVC_VERSION_TOOLSET_SEARCH_MAP[version_key] = []
     if vs_def.vs_express:
@@ -305,11 +368,11 @@
 for vs_def in VISUALSTUDIO_DEFINITION_LIST:
     if not vs_def.vc_buildtools_def.vc_istoolset:
         continue
-    version_key = vs_def.vc_buildtools_def.vc_version
+    version_key = vs_def.vc_buildtools_def.msvc_version
     for vc_buildtools in vs_def.vc_buildtools_all:
         toolset_buildtools_def = MSVC_BUILDTOOLS_INTERNAL[vc_buildtools]
-        toolset_vs_def = MSVC_VERSION_INTERNAL[toolset_buildtools_def.vc_version]
-        buildtools_key = toolset_buildtools_def.vc_version
+        toolset_vs_def = MSVC_VERSION_INTERNAL[toolset_buildtools_def.msvc_version]
+        buildtools_key = toolset_buildtools_def.msvc_version
         MSVC_VERSION_TOOLSET_SEARCH_MAP[buildtools_key].extend(MSVC_VERSION_TOOLSET_DEFAULTS_MAP[version_key])
 
 # convert string version set to string version list ranked in descending order
diff --git a/SCons/Tool/MSCommon/MSVC/Kind.py b/SCons/Tool/MSCommon/MSVC/Kind.py
new file mode 100644
index 0000000000..47d4a25d9f
--- /dev/null
+++ b/SCons/Tool/MSCommon/MSVC/Kind.py
@@ -0,0 +1,668 @@
+# MIT License
+#
+# Copyright The SCons Foundation
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""
+Version kind categorization for Microsoft Visual C/C++.
+"""
+
+import os
+import re
+
+from collections import (
+    namedtuple,
+)
+
+from ..common import (
+    debug,
+)
+
+from . import Registry
+from . import Util
+
+from . import Dispatcher
+Dispatcher.register_modulename(__name__)
+
+
+# use express install for non-express msvc_version if no other installations found
+USE_EXPRESS_FOR_NONEXPRESS = True
+
+# productdir kind
+
+VCVER_KIND_UNKNOWN = 0  # undefined
+VCVER_KIND_DEVELOP = 1  # devenv binary
+VCVER_KIND_EXPRESS = 2  # express binary
+VCVER_KIND_BTDISPATCH = 3  # no ide binaries (buildtools dispatch folder)
+VCVER_KIND_VCFORPYTHON = 4  # no ide binaries (2008/9.0)
+VCVER_KIND_EXPRESS_WIN = 5  # express for windows binary (VSWinExpress)
+VCVER_KIND_EXPRESS_WEB = 6  # express for web binary (VWDExpress)
+VCVER_KIND_SDK = 7  # no ide binaries
+VCVER_KIND_CMDLINE = 8  # no ide binaries
+
+VCVER_KIND_STR = {
+    VCVER_KIND_UNKNOWN: '<Unknown>',
+    VCVER_KIND_DEVELOP: 'Develop',
+    VCVER_KIND_EXPRESS: 'Express',
+    VCVER_KIND_BTDISPATCH: 'BTDispatch',
+    VCVER_KIND_VCFORPYTHON: 'VCForPython',
+    VCVER_KIND_EXPRESS_WIN: 'Express-Win',
+    VCVER_KIND_EXPRESS_WEB: 'Express-Web',
+    VCVER_KIND_SDK: 'SDK',
+    VCVER_KIND_CMDLINE: 'CmdLine',
+}
+
+BITFIELD_KIND_DEVELOP     = 0b_1000
+BITFIELD_KIND_EXPRESS     = 0b_0100
+BITFIELD_KIND_EXPRESS_WIN = 0b_0010
+BITFIELD_KIND_EXPRESS_WEB = 0b_0001
+
+VCVER_KIND_PROGRAM = namedtuple("VCVerKindProgram", [
+    'kind',  # relpath from pdir to vsroot
+    'program',  # ide binaries
+    'bitfield',
+])
+
+#
+
+IDE_PROGRAM_DEVENV_COM = VCVER_KIND_PROGRAM(
+    kind=VCVER_KIND_DEVELOP,
+    program='devenv.com',
+    bitfield=BITFIELD_KIND_DEVELOP,
+)
+
+IDE_PROGRAM_MSDEV_COM = VCVER_KIND_PROGRAM(
+    kind=VCVER_KIND_DEVELOP,
+    program='msdev.com',
+    bitfield=BITFIELD_KIND_DEVELOP,
+)
+
+IDE_PROGRAM_WDEXPRESS_EXE = VCVER_KIND_PROGRAM(
+    kind=VCVER_KIND_EXPRESS,
+    program='WDExpress.exe',
+    bitfield=BITFIELD_KIND_EXPRESS,
+)
+
+IDE_PROGRAM_VCEXPRESS_EXE = VCVER_KIND_PROGRAM(
+    kind=VCVER_KIND_EXPRESS,
+    program='VCExpress.exe',
+    bitfield=BITFIELD_KIND_EXPRESS,
+)
+
+IDE_PROGRAM_VSWINEXPRESS_EXE = VCVER_KIND_PROGRAM(
+    kind=VCVER_KIND_EXPRESS_WIN,
+    program='VSWinExpress.exe',
+    bitfield=BITFIELD_KIND_EXPRESS_WIN,
+)
+
+IDE_PROGRAM_VWDEXPRESS_EXE = VCVER_KIND_PROGRAM(
+    kind=VCVER_KIND_EXPRESS_WEB,
+    program='VWDExpress.exe',
+    bitfield=BITFIELD_KIND_EXPRESS_WEB,
+)
+
+# detection configuration
+
+VCVER_KIND_DETECT = namedtuple("VCVerKindDetect", [
+    'root',  # relpath from pdir to vsroot
+    'path',  # vsroot to ide dir
+    'programs',  # ide binaries
+])
+
+# detected binaries
+
+VCVER_DETECT_BINARIES = namedtuple("VCVerDetectBinaries", [
+    'bitfields',  # detect values
+    'have_dev',  # develop ide binary
+    'have_exp',  # express ide binary
+    'have_exp_win',  # express windows ide binary
+    'have_exp_web',  # express web ide binary
+])
+
+
+VCVER_DETECT_KIND = namedtuple("VCVerDetectKind", [
+    'skip',  # skip vs root
+    'save',  # save in case no other kind found
+    'kind',  # internal kind
+    'binaries_t',
+    'extended',
+])
+
+# unknown value
+
+_VCVER_DETECT_KIND_UNKNOWN = VCVER_DETECT_KIND(
+    skip=True,
+    save=False,
+    kind=VCVER_KIND_UNKNOWN,
+    binaries_t=VCVER_DETECT_BINARIES(
+        bitfields=0b0,
+        have_dev=False,
+        have_exp=False,
+        have_exp_win=False,
+        have_exp_web=False,
+    ),
+    extended={},
+)
+
+#
+
+_msvc_pdir_func = None
+
+def register_msvc_version_pdir_func(func):
+    global _msvc_pdir_func
+    if func:
+        _msvc_pdir_func = func
+
+_cache_vcver_kind_map = {}
+
+def msvc_version_register_kind(msvc_version, kind_t) -> None:
+    global _cache_vcver_kind_map
+    if kind_t is None:
+        kind_t = _VCVER_DETECT_KIND_UNKNOWN
+    debug('msvc_version=%s, kind=%s', repr(msvc_version), repr(VCVER_KIND_STR[kind_t.kind]))
+    _cache_vcver_kind_map[msvc_version] = kind_t
+
+def _msvc_version_kind_lookup(msvc_version, env=None):
+    global _cache_vcver_kind_map
+    global _msvc_pdir_func
+    if msvc_version not in _cache_vcver_kind_map:
+        _msvc_pdir_func(msvc_version, env)
+    kind_t = _cache_vcver_kind_map.get(msvc_version, _VCVER_DETECT_KIND_UNKNOWN)
+    debug(
+        'kind=%s, dev=%s, exp=%s, msvc_version=%s',
+        repr(VCVER_KIND_STR[kind_t.kind]),
+        kind_t.binaries_t.have_dev, kind_t.binaries_t.have_exp,
+        repr(msvc_version)
+    )
+    return kind_t
+
+def msvc_version_is_btdispatch(msvc_version, env=None):
+    kind_t = _msvc_version_kind_lookup(msvc_version, env)
+    is_btdispatch = bool(kind_t.kind == VCVER_KIND_BTDISPATCH)
+    debug(
+        'is_btdispatch=%s, kind:%s, msvc_version=%s',
+        repr(is_btdispatch), repr(VCVER_KIND_STR[kind_t.kind]), repr(msvc_version)
+    )
+    return is_btdispatch
+
+def msvc_version_is_express(msvc_version, env=None):
+    kind_t = _msvc_version_kind_lookup(msvc_version, env)
+    is_express = bool(kind_t.kind == VCVER_KIND_EXPRESS)
+    debug(
+        'is_express=%s, kind:%s, msvc_version=%s',
+        repr(is_express), repr(VCVER_KIND_STR[kind_t.kind]), repr(msvc_version)
+    )
+    return is_express
+
+def msvc_version_is_vcforpython(msvc_version, env=None):
+    kind_t = _msvc_version_kind_lookup(msvc_version, env)
+    is_vcforpython = bool(kind_t.kind == VCVER_KIND_VCFORPYTHON)
+    debug(
+        'is_vcforpython=%s, kind:%s, msvc_version=%s',
+        repr(is_vcforpython), repr(VCVER_KIND_STR[kind_t.kind]), repr(msvc_version)
+    )
+    return is_vcforpython
+
+def msvc_version_skip_uwp_target(env, msvc_version):
+
+    vernum = float(Util.get_msvc_version_prefix(msvc_version))
+    vernum_int = int(vernum * 10)
+
+    if vernum_int != 140:
+        return False
+
+    kind_t = _msvc_version_kind_lookup(msvc_version, env)
+    if kind_t.kind != VCVER_KIND_EXPRESS:
+        return False
+
+    target_arch = env.get('TARGET_ARCH')
+
+    uwp_is_supported = kind_t.extended.get('uwp_is_supported', {})
+    is_supported = uwp_is_supported.get(target_arch, True)
+
+    if is_supported:
+        return False
+
+    return True
+
+def _pdir_detect_binaries(pdir, detect):
+
+    vs_root = os.path.join(pdir, detect.root)
+    ide_path = os.path.join(vs_root, detect.path)
+
+    bitfields = 0b_0000
+    for ide_program in detect.programs:
+        prog = os.path.join(ide_path, ide_program.program)
+        if not os.path.exists(prog):
+            continue
+        bitfields |= ide_program.bitfield
+
+    have_dev = bool(bitfields & BITFIELD_KIND_DEVELOP)
+    have_exp = bool(bitfields & BITFIELD_KIND_EXPRESS)
+    have_exp_win = bool(bitfields & BITFIELD_KIND_EXPRESS_WIN)
+    have_exp_web = bool(bitfields & BITFIELD_KIND_EXPRESS_WEB)
+
+    binaries_t = VCVER_DETECT_BINARIES(
+        bitfields=bitfields,
+        have_dev=have_dev,
+        have_exp=have_exp,
+        have_exp_win=have_exp_win,
+        have_exp_web=have_exp_web,
+    )
+
+    debug(
+        'vs_root=%s, dev=%s, exp=%s, exp_win=%s, exp_web=%s, pdir=%s',
+        repr(vs_root),
+        binaries_t.have_dev, binaries_t.have_exp,
+        binaries_t.have_exp_win, binaries_t.have_exp_web,
+        repr(pdir)
+    )
+
+    return vs_root, binaries_t
+
+_cache_pdir_vswhere_kind = {}
+
+def msvc_version_pdir_vswhere_kind(msvc_version, pdir, detect_t):
+    global _cache_pdir_vswhere_kind
+
+    vc_dir = os.path.normcase(os.path.normpath(pdir))
+    cache_key = (msvc_version, vc_dir)
+
+    rval = _cache_pdir_vswhere_kind.get(cache_key)
+    if rval is not None:
+        debug('cache=%s', repr(rval))
+        return rval
+
+    extended = {}
+
+    prefix, suffix = Util.get_msvc_version_prefix_suffix(msvc_version)
+
+    vs_root, binaries_t = _pdir_detect_binaries(pdir, detect_t)
+
+    if binaries_t.have_dev:
+        kind = VCVER_KIND_DEVELOP
+    elif binaries_t.have_exp:
+        kind = VCVER_KIND_EXPRESS
+    else:
+        kind = VCVER_KIND_CMDLINE
+
+    skip = False
+    save = False
+
+    if suffix != 'Exp' and kind == VCVER_KIND_EXPRESS:
+        skip = True
+        save = USE_EXPRESS_FOR_NONEXPRESS
+    elif suffix == 'Exp' and kind != VCVER_KIND_EXPRESS:
+        skip = True
+
+    kind_t = VCVER_DETECT_KIND(
+        skip=skip,
+        save=save,
+        kind=kind,
+        binaries_t=binaries_t,
+        extended=extended,
+    )
+
+    debug(
+        'skip=%s, save=%s, kind=%s, msvc_version=%s, pdir=%s',
+        kind_t.skip, kind_t.save, repr(VCVER_KIND_STR[kind_t.kind]),
+        repr(msvc_version), repr(pdir)
+    )
+
+    _cache_pdir_vswhere_kind[cache_key] = kind_t
+
+    return kind_t
+
+# VS2015 buildtools batch file call detection
+#    vs2015 buildtools do not support sdk_version or UWP arguments
+
+_VS2015BT_PATH = r'..\Microsoft Visual C++ Build Tools\vcbuildtools.bat'
+
+_VS2015BT_REGEX_STR = ''.join([
+    r'^\s*if\s+exist\s+',
+    re.escape(fr'"%~dp0..\{_VS2015BT_PATH}"'),
+    r'\s+goto\s+setup_buildsku\s*$',
+])
+
+_VS2015BT_VCVARS_BUILDTOOLS = re.compile(_VS2015BT_REGEX_STR, re.IGNORECASE)
+_VS2015BT_VCVARS_STOP = re.compile(r'^\s*[:]Setup_VS\s*$', re.IGNORECASE)
+
+def _vs_buildtools_2015_vcvars(vcvars_file):
+    have_buildtools_vcvars = False
+    with open(vcvars_file) as fh:
+        for line in fh:
+            if _VS2015BT_VCVARS_BUILDTOOLS.match(line):
+                have_buildtools_vcvars = True
+                break
+            if _VS2015BT_VCVARS_STOP.match(line):
+                break
+    return have_buildtools_vcvars
+
+def _vs_buildtools_2015(vs_root, vc_dir):
+
+    is_btdispatch = False
+
+    do_once = True
+    while do_once:
+        do_once = False
+
+        buildtools_file = os.path.join(vs_root, _VS2015BT_PATH)
+        have_buildtools = os.path.exists(buildtools_file)
+        debug('have_buildtools=%s', have_buildtools)
+        if not have_buildtools:
+            break
+
+        vcvars_file = os.path.join(vc_dir, 'vcvarsall.bat')
+        have_vcvars = os.path.exists(vcvars_file)
+        debug('have_vcvars=%s', have_vcvars)
+        if not have_vcvars:
+            break
+
+        have_buildtools_vcvars = _vs_buildtools_2015_vcvars(vcvars_file)
+        debug('have_buildtools_vcvars=%s', have_buildtools_vcvars)
+        if not have_buildtools_vcvars:
+            break
+
+        is_btdispatch = True
+
+    debug('is_btdispatch=%s', is_btdispatch)
+    return is_btdispatch
+
+_VS2015EXP_VCVARS_LIBPATH = re.compile(
+    ''.join([
+        r'^\s*\@if\s+exist\s+\"\%VCINSTALLDIR\%LIB\\store\\(amd64|arm)"\s+',
+        r'set (LIB|LIBPATH)=\%VCINSTALLDIR\%LIB\\store\\(amd64|arm);.*\%(LIB|LIBPATH)\%\s*$'
+    ]),
+    re.IGNORECASE
+)
+
+_VS2015EXP_VCVARS_STOP = re.compile(r'^\s*[:]GetVSCommonToolsDir\s*$', re.IGNORECASE)
+
+def _vs_express_2015_vcvars(vcvars_file):
+    n_libpath = 0
+    with open(vcvars_file) as fh:
+        for line in fh:
+            if _VS2015EXP_VCVARS_LIBPATH.match(line):
+                n_libpath += 1
+            elif _VS2015EXP_VCVARS_STOP.match(line):
+                break
+    have_uwp_fix = n_libpath >= 2
+    return have_uwp_fix
+
+def _vs_express_2015(pdir):
+
+    have_uwp_amd64 = False
+    have_uwp_arm = False
+
+    vcvars_file = os.path.join(pdir, r'vcvarsall.bat')
+    if os.path.exists(vcvars_file):
+
+        vcvars_file = os.path.join(pdir, r'bin\x86_amd64\vcvarsx86_amd64.bat')
+        if os.path.exists(vcvars_file):
+            have_uwp_fix = _vs_express_2015_vcvars(vcvars_file)
+            if have_uwp_fix:
+                have_uwp_amd64 = True
+
+        vcvars_file = os.path.join(pdir, r'bin\x86_arm\vcvarsx86_arm.bat')
+        if os.path.exists(vcvars_file):
+            have_uwp_fix = _vs_express_2015_vcvars(vcvars_file)
+            if have_uwp_fix:
+                have_uwp_arm = True
+
+    debug('have_uwp_amd64=%s, have_uwp_arm=%s', have_uwp_amd64, have_uwp_arm)
+    return have_uwp_amd64, have_uwp_arm
+
+# winsdk installed 2010 [7.1], 2008 [7.0, 6.1] folders
+
+_REGISTRY_WINSDK_VERSIONS = {'10.0', '9.0'}
+
+_cache_pdir_registry_winsdk = {}
+
+def _msvc_version_pdir_registry_winsdk(msvc_version, pdir):
+    global _cache_pdir_registry_winsdk
+
+    # detect winsdk-only installations
+    #
+    #     registry keys:
+    #         [prefix]\VisualStudio\SxS\VS7\10.0  <undefined>
+    #         [prefix]\VisualStudio\SxS\VC7\10.0  product directory
+    #         [prefix]\VisualStudio\SxS\VS7\9.0   <undefined>
+    #         [prefix]\VisualStudio\SxS\VC7\9.0   product directory
+    #
+    #     winsdk notes:
+    #         - winsdk installs do not define the common tools env var
+    #         - the product dir is detected but the vcvars batch files will fail
+    #         - regular installations populate the VS7 registry keys
+    #
+
+    vc_dir = os.path.normcase(os.path.normpath(pdir))
+    cache_key = (msvc_version, vc_dir)
+
+    rval = _cache_pdir_registry_winsdk.get(cache_key)
+    if rval is not None:
+        debug('cache=%s', repr(rval))
+        return rval
+
+    if msvc_version not in _REGISTRY_WINSDK_VERSIONS:
+
+        is_sdk = False
+
+        debug('is_sdk=%s, msvc_version=%s', is_sdk, repr(msvc_version))
+
+    else:
+
+        vc_dir = os.path.normcase(os.path.normpath(pdir))
+
+        vc_suffix = Registry.vstudio_sxs_vc7(msvc_version)
+        vc_qresults = [record[0] for record in Registry.microsoft_query_paths(vc_suffix)]
+        vc_root = os.path.normcase(os.path.normpath(vc_qresults[0])) if vc_qresults else None
+
+        if vc_dir != vc_root:
+            # registry vc path is not the current pdir
+
+            is_sdk = False
+
+            debug(
+                'is_sdk=%s, msvc_version=%s, pdir=%s, vc_root=%s',
+                is_sdk, repr(msvc_version), repr(vc_dir), repr(vc_root)
+            )
+
+        else:
+            # registry vc path is the current pdir
+
+            vs_suffix = Registry.vstudio_sxs_vs7(msvc_version)
+            vs_qresults = [record[0] for record in Registry.microsoft_query_paths(vs_suffix)]
+            vs_root = vs_qresults[0] if vs_qresults else None
+
+            is_sdk = bool(not vs_root and vc_root)
+
+            debug(
+                'is_sdk=%s, msvc_version=%s, vs_root=%s, vc_root=%s',
+                is_sdk, repr(msvc_version), repr(vs_root), repr(vc_root)
+            )
+
+    _cache_pdir_registry_winsdk[cache_key] = is_sdk
+
+    return is_sdk
+
+_cache_pdir_registry_kind = {}
+
+def msvc_version_pdir_registry_kind(msvc_version, pdir, detect_t, is_vcforpython=False):
+    global _cache_pdir_registry_kind
+
+    vc_dir = os.path.normcase(os.path.normpath(pdir))
+    cache_key = (msvc_version, vc_dir)
+
+    rval = _cache_pdir_registry_kind.get(cache_key)
+    if rval is not None:
+        debug('cache=%s', repr(rval))
+        return rval
+
+    extended = {}
+
+    prefix, suffix = Util.get_msvc_version_prefix_suffix(msvc_version)
+
+    vs_root, binaries_t = _pdir_detect_binaries(pdir, detect_t)
+
+    if binaries_t.have_dev:
+        kind = VCVER_KIND_DEVELOP
+    elif binaries_t.have_exp:
+        kind = VCVER_KIND_EXPRESS
+    elif msvc_version == '14.0' and _vs_buildtools_2015(vs_root, pdir):
+        kind = VCVER_KIND_BTDISPATCH
+    elif msvc_version == '9.0' and is_vcforpython:
+        kind = VCVER_KIND_VCFORPYTHON
+    elif binaries_t.have_exp_win:
+        kind = VCVER_KIND_EXPRESS_WIN
+    elif binaries_t.have_exp_web:
+        kind = VCVER_KIND_EXPRESS_WEB
+    elif _msvc_version_pdir_registry_winsdk(msvc_version, pdir):
+        kind = VCVER_KIND_SDK
+    else:
+        kind = VCVER_KIND_CMDLINE
+
+    skip = False
+    save = False
+
+    if kind in (VCVER_KIND_EXPRESS_WIN, VCVER_KIND_EXPRESS_WEB, VCVER_KIND_SDK):
+        skip = True
+    elif suffix != 'Exp' and kind == VCVER_KIND_EXPRESS:
+        skip = True
+        save = USE_EXPRESS_FOR_NONEXPRESS
+    elif suffix == 'Exp' and kind != VCVER_KIND_EXPRESS:
+        skip = True
+
+    if prefix == '14.0' and kind == VCVER_KIND_EXPRESS:
+        have_uwp_amd64, have_uwp_arm = _vs_express_2015(pdir)
+        uwp_is_supported = {
+            'x86': True,
+            'amd64': have_uwp_amd64,
+            'arm': have_uwp_arm,
+        }
+        extended['uwp_is_supported'] = uwp_is_supported
+
+    kind_t = VCVER_DETECT_KIND(
+        skip=skip,
+        save=save,
+        kind=kind,
+        binaries_t=binaries_t,
+        extended=extended,
+    )
+
+    debug(
+        'skip=%s, save=%s, kind=%s, msvc_version=%s, pdir=%s',
+        kind_t.skip, kind_t.save, repr(VCVER_KIND_STR[kind_t.kind]),
+        repr(msvc_version), repr(pdir)
+    )
+
+    _cache_pdir_registry_kind[cache_key] = kind_t
+
+    return kind_t
+
+# queries
+
+def get_msvc_version_kind(msvc_version, env=None):
+    kind_t = _msvc_version_kind_lookup(msvc_version, env)
+    kind_str = VCVER_KIND_STR[kind_t.kind]
+    debug(
+        'kind=%s, kind_str=%s, msvc_version=%s',
+        repr(kind_t.kind), repr(kind_str), repr(msvc_version)
+    )
+    return (kind_t.kind, kind_str)
+
+def msvc_version_sdk_version_is_supported(msvc_version, env=None):
+
+    vernum = float(Util.get_msvc_version_prefix(msvc_version))
+    vernum_int = int(vernum * 10)
+
+    kind_t = _msvc_version_kind_lookup(msvc_version, env)
+
+    if vernum_int >= 141:
+        # VS2017 and later
+        is_supported = True
+    elif vernum_int == 140:
+        # VS2015:
+        #   True:  Develop, CmdLine
+        #   False: Express, BTDispatch
+        is_supported = True
+        if kind_t.kind == VCVER_KIND_EXPRESS:
+            is_supported = False
+        elif kind_t.kind == VCVER_KIND_BTDISPATCH:
+            is_supported = False
+    else:
+        # VS2013 and earlier
+        is_supported = False
+
+    debug(
+        'is_supported=%s, msvc_version=%s, kind=%s',
+        is_supported, repr(msvc_version), repr(VCVER_KIND_STR[kind_t.kind])
+    )
+    return is_supported
+
+def msvc_version_uwp_is_supported(msvc_version, target_arch=None, env=None):
+
+    vernum = float(Util.get_msvc_version_prefix(msvc_version))
+    vernum_int = int(vernum * 10)
+
+    kind_t = _msvc_version_kind_lookup(msvc_version, env)
+
+    is_target = False
+
+    if vernum_int >= 141:
+        # VS2017 and later
+        is_supported = True
+    elif vernum_int == 140:
+        # VS2015:
+        #   True:  Develop, CmdLine
+        #   Maybe: Express
+        #   False: BTDispatch
+        is_supported = True
+        if kind_t.kind == VCVER_KIND_EXPRESS:
+            uwp_is_supported = kind_t.extended.get('uwp_is_supported', {})
+            is_supported = uwp_is_supported.get(target_arch, True)
+            is_target = True
+        elif kind_t.kind == VCVER_KIND_BTDISPATCH:
+            is_supported = False
+    else:
+        # VS2013 and earlier
+        is_supported = False
+
+    debug(
+        'is_supported=%s, is_target=%s, msvc_version=%s, kind=%s, target_arch=%s',
+        is_supported, is_target, repr(msvc_version), repr(VCVER_KIND_STR[kind_t.kind]), repr(target_arch)
+    )
+
+    return is_supported, is_target
+
+# reset cache
+
+def reset() -> None:
+    global _cache_vcver_kind_map
+    global _cache_pdir_vswhere_kind
+    global _cache_pdir_registry_kind
+    global _cache_pdir_registry_winsdk
+
+    debug('')
+
+    _cache_vcver_kind_map = {}
+    _cache_pdir_vswhere_kind = {}
+    _cache_pdir_registry_kind = {}
+    _cache_pdir_registry_winsdk = {}
diff --git a/SCons/Tool/MSCommon/MSVC/Policy.py b/SCons/Tool/MSCommon/MSVC/Policy.py
index fe8da3156b..7e11a63ba5 100644
--- a/SCons/Tool/MSCommon/MSVC/Policy.py
+++ b/SCons/Tool/MSCommon/MSVC/Policy.py
@@ -38,6 +38,10 @@
     namedtuple,
 )
 
+from contextlib import (
+    contextmanager,
+)
+
 import SCons.Warnings
 
 from ..common import (
@@ -215,6 +219,18 @@ def msvc_notfound_handler(env, msg):
     else:
         SCons.Warnings.warn(SCons.Warnings.VisualCMissingWarning, msg)
 
+@contextmanager
+def msvc_notfound_policy_contextmanager(MSVC_NOTFOUND_POLICY=None):
+    """ Temporarily change the MSVC not found policy within a context.
+
+    Args:
+        MSVC_NOTFOUND_POLICY:
+           string representing the policy behavior
+           when MSVC is not found or None
+    """
+    prev_policy = msvc_set_notfound_policy(MSVC_NOTFOUND_POLICY)
+    yield
+    msvc_set_notfound_policy(prev_policy)
 
 def _msvc_scripterror_policy_lookup(symbol):
 
@@ -299,3 +315,16 @@ def msvc_scripterror_handler(env, msg):
     else:
         SCons.Warnings.warn(MSVCScriptExecutionWarning, msg)
 
+@contextmanager
+def msvc_scripterror_policy_contextmanager(MSVC_SCRIPTERROR_POLICY=None):
+    """ Temporarily change the msvc batch execution errors policy within a context.
+
+    Args:
+        MSVC_SCRIPTERROR_POLICY:
+           string representing the policy behavior
+           when msvc batch file execution errors are detected or None
+    """
+    prev_policy = msvc_set_scripterror_policy(MSVC_SCRIPTERROR_POLICY)
+    yield
+    msvc_set_scripterror_policy(prev_policy)
+
diff --git a/SCons/Tool/MSCommon/MSVC/Registry.py b/SCons/Tool/MSCommon/MSVC/Registry.py
index f9e544c6fc..b5b72c42fc 100644
--- a/SCons/Tool/MSCommon/MSVC/Registry.py
+++ b/SCons/Tool/MSCommon/MSVC/Registry.py
@@ -110,6 +110,9 @@ def windows_kit_query_paths(version):
     q = windows_kits(version)
     return microsoft_query_paths(q)
 
+def vstudio_sxs_vs7(version):
+    return '\\'.join([r'VisualStudio\SxS\VS7', version])
+
 def vstudio_sxs_vc7(version):
     return '\\'.join([r'VisualStudio\SxS\VC7', version])
 
diff --git a/SCons/Tool/MSCommon/MSVC/ScriptArguments.py b/SCons/Tool/MSCommon/MSVC/ScriptArguments.py
index 8848095cf9..2c766fed4e 100644
--- a/SCons/Tool/MSCommon/MSVC/ScriptArguments.py
+++ b/SCons/Tool/MSCommon/MSVC/ScriptArguments.py
@@ -42,6 +42,7 @@
 from . import Config
 from . import Registry
 from . import WinSDK
+from . import Kind
 
 from .Exceptions import (
     MSVCInternalError,
@@ -172,6 +173,12 @@ class SortOrder(enum.IntEnum):
     'vs_def',
 ])
 
+TOOLSET_VERSION_ARGS_DEFINITION = namedtuple('ToolsetVersionArgsDefinition', [
+    'version', # full version (e.g., '14.1Exp', '14.32.31326')
+    'vc_buildtools_def',
+    'is_user',
+])
+
 def _msvc_version(version):
 
     verstr = Util.get_msvc_version_prefix(version)
@@ -184,19 +191,22 @@ def _msvc_version(version):
 
     return version_args
 
-def _toolset_version(version):
+def _toolset_version(version, is_user=False):
 
-    verstr = Util.get_msvc_version_prefix(version)
-    vs_def = Config.MSVC_VERSION_INTERNAL[verstr]
+    vc_series = Util.get_msvc_version_prefix(version)
 
-    version_args = MSVC_VERSION_ARGS_DEFINITION(
+    vc_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL[vc_series]
+    vc_buildtools_def = Config.VC_BUILDTOOLS_MAP[vc_buildseries_def.vc_buildseries]
+
+    version_args = TOOLSET_VERSION_ARGS_DEFINITION(
         version = version,
-        vs_def = vs_def,
+        vc_buildtools_def = vc_buildtools_def,
+        is_user = is_user,
     )
 
     return version_args
 
-def _msvc_script_argument_uwp(env, msvc, arglist):
+def _msvc_script_argument_uwp(env, msvc, arglist, target_arch):
 
     uwp_app = env['MSVC_UWP_APP']
     debug('MSVC_VERSION=%s, MSVC_UWP_APP=%s', repr(msvc.version), repr(uwp_app))
@@ -207,17 +217,34 @@ def _msvc_script_argument_uwp(env, msvc, arglist):
     if uwp_app not in _ARGUMENT_BOOLEAN_TRUE_LEGACY:
         return None
 
-    if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric:
+    if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric:
         debug(
             'invalid: msvc version constraint: %s < %s VS2015',
-            repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric),
-            repr(VS2015.vc_buildtools_def.vc_version_numeric)
+            repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric),
+            repr(VS2015.vc_buildtools_def.msvc_version_numeric)
         )
         err_msg = "MSVC_UWP_APP ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format(
-            repr(uwp_app), repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version)
+            repr(uwp_app), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version)
         )
         raise MSVCArgumentError(err_msg)
 
+    is_supported, is_target = Kind.msvc_version_uwp_is_supported(msvc.version, target_arch, env)
+    if not is_supported:
+        _, kind_str = Kind.get_msvc_version_kind(msvc.version)
+        debug(
+            'invalid: msvc_version constraint: %s %s %s',
+            repr(msvc.version), repr(kind_str), repr(target_arch)
+        )
+        if is_target and target_arch:
+            err_msg = "MSVC_UWP_APP ({}) TARGET_ARCH ({}) is not supported for MSVC_VERSION {} ({})".format(
+                repr(uwp_app), repr(target_arch), repr(msvc.version), repr(kind_str)
+            )
+        else:
+            err_msg = "MSVC_UWP_APP ({}) is not supported for MSVC_VERSION {} ({})".format(
+                repr(uwp_app), repr(msvc.version), repr(kind_str)
+            )
+        raise MSVCArgumentError(err_msg)
+
     # VS2017+ rewrites uwp => store for 14.0 toolset
     uwp_arg = msvc.vs_def.vc_uwp
 
@@ -250,16 +277,24 @@ def _user_script_argument_uwp(env, uwp, user_argstr) -> bool:
 
     raise MSVCArgumentError(err_msg)
 
-def _msvc_script_argument_sdk_constraints(msvc, sdk_version):
+def _msvc_script_argument_sdk_constraints(msvc, sdk_version, env):
 
-    if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric:
+    if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric:
         debug(
             'invalid: msvc_version constraint: %s < %s VS2015',
-            repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric),
-            repr(VS2015.vc_buildtools_def.vc_version_numeric)
+            repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric),
+            repr(VS2015.vc_buildtools_def.msvc_version_numeric)
         )
         err_msg = "MSVC_SDK_VERSION ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format(
-            repr(sdk_version), repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version)
+            repr(sdk_version), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version)
+        )
+        return err_msg
+
+    if not Kind.msvc_version_sdk_version_is_supported(msvc.version, env):
+        _, kind_str = Kind.get_msvc_version_kind(msvc.version)
+        debug('invalid: msvc_version constraint: %s %s', repr(msvc.version), repr(kind_str))
+        err_msg = "MSVC_SDK_VERSION ({}) is not supported for MSVC_VERSION {} ({})".format(
+            repr(sdk_version), repr(msvc.version), repr(kind_str)
         )
         return err_msg
 
@@ -277,23 +312,23 @@ def _msvc_script_argument_sdk_platform_constraints(msvc, toolset, sdk_version, p
 
     if sdk_version == '8.1' and platform_def.is_uwp:
 
-        vs_def = toolset.vs_def if toolset else msvc.vs_def
+        vc_buildtools_def = toolset.vc_buildtools_def if toolset else msvc.vs_def.vc_buildtools_def
 
-        if vs_def.vc_buildtools_def.vc_version_numeric > VS2015.vc_buildtools_def.vc_version_numeric:
+        if vc_buildtools_def.msvc_version_numeric > VS2015.vc_buildtools_def.msvc_version_numeric:
             debug(
                 'invalid: uwp/store SDK 8.1 msvc_version constraint: %s > %s VS2015',
-                repr(vs_def.vc_buildtools_def.vc_version_numeric),
-                repr(VS2015.vc_buildtools_def.vc_version_numeric)
+                repr(vc_buildtools_def.msvc_version_numeric),
+                repr(VS2015.vc_buildtools_def.msvc_version_numeric)
             )
-            if toolset and toolset.vs_def != msvc.vs_def:
-                err_msg = "MSVC_SDK_VERSION ({}) and platform type ({}) constraint violation: toolset version {} > {} VS2015".format(
+            if toolset and toolset.is_user:
+                err_msg = "MSVC_SDK_VERSION ({}) and platform type ({}) constraint violation: toolset {} MSVC_VERSION {} > {} VS2015".format(
                     repr(sdk_version), repr(platform_def.vc_platform),
-                    repr(toolset.version), repr(VS2015.vc_buildtools_def.vc_version)
+                    repr(toolset.version), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version)
                 )
             else:
                 err_msg = "MSVC_SDK_VERSION ({}) and platform type ({}) constraint violation: MSVC_VERSION {} > {} VS2015".format(
                     repr(sdk_version), repr(platform_def.vc_platform),
-                    repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version)
+                    repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version)
                 )
             return err_msg
 
@@ -310,7 +345,7 @@ def _msvc_script_argument_sdk(env, msvc, toolset, platform_def, arglist):
     if not sdk_version:
         return None
 
-    err_msg = _msvc_script_argument_sdk_constraints(msvc, sdk_version)
+    err_msg = _msvc_script_argument_sdk_constraints(msvc, sdk_version, env)
     if err_msg:
         raise MSVCArgumentError(err_msg)
 
@@ -333,7 +368,10 @@ def _msvc_script_argument_sdk(env, msvc, toolset, platform_def, arglist):
 
 def _msvc_script_default_sdk(env, msvc, platform_def, arglist, force_sdk: bool=False):
 
-    if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric:
+    if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric:
+        return None
+
+    if not Kind.msvc_version_sdk_version_is_supported(msvc.version, env):
         return None
 
     sdk_list = WinSDK.get_sdk_version_list(msvc.vs_def, platform_def)
@@ -415,10 +453,11 @@ def _msvc_sxs_toolset_folder(msvc, sxs_folder):
     if Util.is_toolset_sxs(sxs_folder):
         return sxs_folder, sxs_folder
 
-    key = (msvc.vs_def.vc_buildtools_def.vc_version, sxs_folder)
-    if key in _msvc_sxs_bugfix_folder:
-        sxs_version = _msvc_sxs_bugfix_folder[key]
-        return sxs_folder, sxs_version
+    for vc_buildseries_def in msvc.vs_def.vc_buildtools_def.vc_buildseries_list:
+        key = (vc_buildseries_def.vc_version, sxs_folder)
+        sxs_version = _msvc_sxs_bugfix_folder.get(key)
+        if sxs_version:
+            return sxs_folder, sxs_version
 
     debug('sxs folder: ignore version=%s', repr(sxs_folder))
     return None, None
@@ -574,49 +613,60 @@ def _msvc_version_toolset_vcvars(msvc, vc_dir, toolset_version):
 
 def _msvc_script_argument_toolset_constraints(msvc, toolset_version):
 
-    if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric:
+    if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric:
         debug(
             'invalid: msvc version constraint: %s < %s VS2017',
-            repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric),
-            repr(VS2017.vc_buildtools_def.vc_version_numeric)
+            repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric),
+            repr(VS2017.vc_buildtools_def.msvc_version_numeric)
         )
         err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: MSVC_VERSION {} < {} VS2017".format(
-            repr(toolset_version), repr(msvc.version), repr(VS2017.vc_buildtools_def.vc_version)
+            repr(toolset_version), repr(msvc.version), repr(VS2017.vc_buildtools_def.msvc_version)
         )
         return err_msg
 
-    toolset_verstr = Util.get_msvc_version_prefix(toolset_version)
+    toolset_series = Util.get_msvc_version_prefix(toolset_version)
 
-    if not toolset_verstr:
+    if not toolset_series:
         debug('invalid: msvc version: toolset_version=%s', repr(toolset_version))
         err_msg = 'MSVC_TOOLSET_VERSION {} format is not supported'.format(
             repr(toolset_version)
         )
         return err_msg
 
-    toolset_vernum = float(toolset_verstr)
+    toolset_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL.get(toolset_series)
+    if not toolset_buildseries_def:
+        debug('invalid: msvc version: toolset_version=%s', repr(toolset_version))
+        err_msg = 'MSVC_TOOLSET_VERSION {} build series {} is not supported'.format(
+            repr(toolset_version), repr(toolset_series)
+        )
+        return err_msg
+
+    toolset_buildtools_def = Config.VC_BUILDTOOLS_MAP[toolset_buildseries_def.vc_buildseries]
 
-    if toolset_vernum < VS2015.vc_buildtools_def.vc_version_numeric:
+    toolset_verstr = toolset_buildtools_def.msvc_version
+    toolset_vernum = toolset_buildtools_def.msvc_version_numeric
+
+    if toolset_vernum < VS2015.vc_buildtools_def.msvc_version_numeric:
         debug(
             'invalid: toolset version constraint: %s < %s VS2015',
-            repr(toolset_vernum), repr(VS2015.vc_buildtools_def.vc_version_numeric)
+            repr(toolset_vernum), repr(VS2015.vc_buildtools_def.msvc_version_numeric)
         )
-        err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} < {} VS2015".format(
-            repr(toolset_version), repr(toolset_verstr), repr(VS2015.vc_buildtools_def.vc_version)
+        err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} < {} VS2015".format(
+            repr(toolset_version), repr(toolset_verstr), repr(VS2015.vc_buildtools_def.msvc_version)
         )
         return err_msg
 
-    if toolset_vernum > msvc.vs_def.vc_buildtools_def.vc_version_numeric:
+    if toolset_vernum > msvc.vs_def.vc_buildtools_def.msvc_version_numeric:
         debug(
             'invalid: toolset version constraint: toolset %s > %s msvc',
-            repr(toolset_vernum), repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric)
+            repr(toolset_vernum), repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric)
         )
-        err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} > {} MSVC_VERSION".format(
+        err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} > {} MSVC_VERSION".format(
             repr(toolset_version), repr(toolset_verstr), repr(msvc.version)
         )
         return err_msg
 
-    if toolset_vernum == VS2015.vc_buildtools_def.vc_version_numeric:
+    if toolset_vernum == VS2015.vc_buildtools_def.msvc_version_numeric:
         # tooset = 14.0
         if Util.is_toolset_full(toolset_version):
             if not Util.is_toolset_140(toolset_version):
@@ -624,7 +674,7 @@ def _msvc_script_argument_toolset_constraints(msvc, toolset_version):
                     'invalid: toolset version 14.0 constraint: %s != 14.0',
                     repr(toolset_version)
                 )
-                err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} != '14.0'".format(
+                err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} != '14.0'".format(
                     repr(toolset_version), repr(toolset_version)
                 )
                 return err_msg
@@ -688,7 +738,7 @@ def _msvc_script_argument_toolset(env, msvc, vc_dir, arglist):
 
 def _msvc_script_default_toolset(env, msvc, vc_dir, arglist, force_toolset: bool=False):
 
-    if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric:
+    if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric:
         return None
 
     toolset_default = _msvc_default_toolset(msvc, vc_dir)
@@ -729,26 +779,26 @@ def _user_script_argument_toolset(env, toolset_version, user_argstr):
 
 def _msvc_script_argument_spectre_constraints(msvc, toolset, spectre_libs, platform_def):
 
-    if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric:
+    if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric:
         debug(
             'invalid: msvc version constraint: %s < %s VS2017',
-            repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric),
-            repr(VS2017.vc_buildtools_def.vc_version_numeric)
+            repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric),
+            repr(VS2017.vc_buildtools_def.msvc_version_numeric)
         )
         err_msg = "MSVC_SPECTRE_LIBS ({}) constraint violation: MSVC_VERSION {} < {} VS2017".format(
-            repr(spectre_libs), repr(msvc.version), repr(VS2017.vc_buildtools_def.vc_version)
+            repr(spectre_libs), repr(msvc.version), repr(VS2017.vc_buildtools_def.msvc_version)
         )
         return err_msg
 
     if toolset:
-        if toolset.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric:
+        if toolset.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric:
             debug(
                 'invalid: toolset version constraint: %s < %s VS2017',
-                repr(toolset.vs_def.vc_buildtools_def.vc_version_numeric),
-                repr(VS2017.vc_buildtools_def.vc_version_numeric)
+                repr(toolset.vc_buildtools_def.msvc_version_numeric),
+                repr(VS2017.vc_buildtools_def.msvc_version_numeric)
             )
             err_msg = "MSVC_SPECTRE_LIBS ({}) constraint violation: toolset version {} < {} VS2017".format(
-                repr(spectre_libs), repr(toolset.version), repr(VS2017.vc_buildtools_def.vc_version)
+                repr(spectre_libs), repr(toolset.version), repr(VS2017.vc_buildtools_def.msvc_version)
             )
             return err_msg
 
@@ -836,14 +886,14 @@ def _msvc_script_argument_user(env, msvc, arglist):
     if not script_args:
         return None
 
-    if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric:
+    if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric:
         debug(
             'invalid: msvc version constraint: %s < %s VS2015',
-            repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric),
-            repr(VS2015.vc_buildtools_def.vc_version_numeric)
+            repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric),
+            repr(VS2015.vc_buildtools_def.msvc_version_numeric)
         )
         err_msg = "MSVC_SCRIPT_ARGS ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format(
-            repr(script_args), repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version)
+            repr(script_args), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version)
         )
         raise MSVCArgumentError(err_msg)
 
@@ -873,7 +923,18 @@ def _msvc_process_construction_variables(env) -> bool:
 
     return False
 
-def msvc_script_arguments(env, version, vc_dir, arg):
+def msvc_script_arguments_has_uwp(env):
+
+    if not _msvc_process_construction_variables(env):
+        return False
+
+    uwp_app = env.get('MSVC_UWP_APP')
+    is_uwp = bool(uwp_app and uwp_app in _ARGUMENT_BOOLEAN_TRUE_LEGACY)
+
+    debug('is_uwp=%s', is_uwp)
+    return is_uwp
+
+def msvc_script_arguments(env, version, vc_dir, arg=None):
 
     arguments = [arg] if arg else []
 
@@ -889,10 +950,12 @@ def msvc_script_arguments(env, version, vc_dir, arg):
 
     if _msvc_process_construction_variables(env):
 
+        target_arch = env.get('TARGET_ARCH')
+
         # MSVC_UWP_APP
 
         if 'MSVC_UWP_APP' in env:
-            uwp = _msvc_script_argument_uwp(env, msvc, arglist)
+            uwp = _msvc_script_argument_uwp(env, msvc, arglist, target_arch)
         else:
             uwp = None
 
@@ -926,7 +989,7 @@ def msvc_script_arguments(env, version, vc_dir, arg):
         if user_toolset:
             toolset = None
         elif toolset_version:
-            toolset = _toolset_version(toolset_version)
+            toolset = _toolset_version(toolset_version, is_user=True)
         elif default_toolset:
             toolset = _toolset_version(default_toolset)
         else:
@@ -958,7 +1021,7 @@ def msvc_script_arguments(env, version, vc_dir, arg):
         if user_argstr:
             _user_script_argument_spectre(env, spectre, user_argstr)
 
-        if msvc.vs_def.vc_buildtools_def.vc_version == '14.0':
+        if msvc.vs_def.vc_buildtools_def.msvc_version == '14.0':
             if user_uwp and sdk_version and len(arglist) == 2:
                 # VS2015 toolset argument order issue: SDK store => store SDK
                 arglist_reverse = True
@@ -1003,6 +1066,14 @@ def _msvc_toolset_versions_internal(msvc_version, vc_dir, full: bool=True, sxs:
 
     return toolset_versions
 
+def _msvc_version_toolsets_internal(msvc_version, vc_dir):
+
+    msvc = _msvc_version(msvc_version)
+
+    toolsets_sxs, toolsets_full = _msvc_version_toolsets(msvc, vc_dir)
+
+    return toolsets_sxs, toolsets_full
+
 def _msvc_toolset_versions_spectre_internal(msvc_version, vc_dir):
 
     msvc = _msvc_version(msvc_version)
diff --git a/SCons/Tool/MSCommon/MSVC/ScriptArgumentsTests.py b/SCons/Tool/MSCommon/MSVC/ScriptArgumentsTests.py
index 670576b046..308e436976 100644
--- a/SCons/Tool/MSCommon/MSVC/ScriptArgumentsTests.py
+++ b/SCons/Tool/MSCommon/MSVC/ScriptArgumentsTests.py
@@ -36,6 +36,10 @@
 from SCons.Tool.MSCommon.MSVC import Util
 from SCons.Tool.MSCommon.MSVC import WinSDK
 from SCons.Tool.MSCommon.MSVC import ScriptArguments
+from SCons.Tool.MSCommon.MSVC.Kind import (
+    msvc_version_is_express,
+    msvc_version_is_btdispatch,
+)
 
 from SCons.Tool.MSCommon.MSVC.Exceptions import (
     MSVCInternalError,
@@ -127,7 +131,7 @@ class Data:
 
     for vcver in Config.MSVC_VERSION_SUFFIX.keys():
         version_def = Util.msvc_version_components(vcver)
-        vc_dir = vc.find_vc_pdir(None, vcver)
+        vc_dir = vc.find_vc_pdir(vcver)
         t = (version_def, vc_dir)
         ALL_VERSIONS_PAIRS.append(t)
         if vc_dir:
@@ -229,7 +233,13 @@ def test_msvc_script_arguments_defaults(self) -> None:
         for version_def, vc_dir in Data.INSTALLED_VERSIONS_PAIRS:
             for arg in ('', 'arch'):
                 scriptargs = func(env, version_def.msvc_version, vc_dir, arg)
-                if version_def.msvc_vernum >= 14.0:
+                sdk_supported = True
+                if version_def.msvc_verstr == '14.0':
+                    if msvc_version_is_express(version_def.msvc_version):
+                        sdk_supported = False
+                    elif msvc_version_is_btdispatch(version_def.msvc_version):
+                        sdk_supported = False
+                if version_def.msvc_vernum >= 14.0 and sdk_supported:
                     if arg and scriptargs.startswith(arg):
                         testargs = scriptargs[len(arg):].lstrip()
                     else:
@@ -289,7 +299,7 @@ def run_msvc_script_args_none(self) -> None:
                 {'MSVC_SCRIPT_ARGS': None, 'MSVC_SPECTRE_LIBS': None},
             ]:
                 env = Environment(**kwargs)
-                _ = func(env, version_def.msvc_version, vc_dir, '')
+                _ = func(env, version_def.msvc_version, vc_dir)
 
     def run_msvc_script_args(self) -> None:
         func = ScriptArguments.msvc_script_arguments
@@ -307,12 +317,17 @@ def run_msvc_script_args(self) -> None:
 
                 toolset_def = toolset_versions[0] if toolset_versions else Util.msvc_extended_version_components(version_def.msvc_verstr)
 
-                earlier_toolset_versions = [toolset_def for toolset_def in toolset_versions if toolset_def.msvc_vernum != version_def.msvc_vernum]
+                earlier_toolset_versions = [earlier_toolset_def for earlier_toolset_def in toolset_versions if earlier_toolset_def.msvc_vernum != version_def.msvc_vernum]
                 earlier_toolset_def = earlier_toolset_versions[0] if earlier_toolset_versions else None
 
+                vc_buildtools_def = Config.MSVC_BUILDTOOLS_EXTERNAL[toolset_def.msvc_buildtools]
+                vc_buildseries_def = vc_buildtools_def.vc_buildseries_list[0]
+
+                latest_buildseries_major, latest_buildseries_minor = [int(comp) for comp in vc_buildseries_def.vc_version.split('.')]
+
                 # should not raise exception (argument not validated)
                 env = Environment(MSVC_SCRIPT_ARGS='undefinedsymbol')
-                _ = func(env, version_def.msvc_version, vc_dir, '')
+                _ = func(env, version_def.msvc_version, vc_dir)
 
                 for kwargs in [
                     {'MSVC_UWP_APP': False, 'MSVC_SCRIPT_ARGS': None},
@@ -323,7 +338,7 @@ def run_msvc_script_args(self) -> None:
                     {'MSVC_SPECTRE_LIBS': 'True', 'MSVC_SCRIPT_ARGS': '-vcvars_spectre_libs=spectre'}, # not boolean ignored
                 ]:
                     env = Environment(**kwargs)
-                    _ = func(env, version_def.msvc_version, vc_dir, '')
+                    _ = func(env, version_def.msvc_version, vc_dir)
 
                 for msvc_uwp_app in (True, False):
 
@@ -362,15 +377,15 @@ def run_msvc_script_args(self) -> None:
                                 (expect, {
                                     'MSVC_SDK_VERSION': sdk_def.sdk_version,
                                     'MSVC_UWP_APP': msvc_uwp_app,
-                                    'MSVC_TOOLSET_VERSION': version_def.msvc_verstr
+                                    'MSVC_TOOLSET_VERSION': toolset_def.msvc_toolset_version
                                 }),
                             ] + more_tests:
                                 env = Environment(**kwargs)
                                 if exc:
                                     with self.assertRaises(MSVCArgumentError):
-                                        _ = func(env, version_def.msvc_version, vc_dir, '')
+                                        _ = func(env, version_def.msvc_version, vc_dir)
                                 else:
-                                    _ = func(env, version_def.msvc_version, vc_dir, '')
+                                    _ = func(env, version_def.msvc_version, vc_dir)
 
                         else:
 
@@ -379,14 +394,14 @@ def run_msvc_script_args(self) -> None:
                                 {'MSVC_SDK_VERSION': sdk_def.sdk_version, 'MSVC_UWP_APP': msvc_uwp_app},
                             ]:
                                 env = Environment(**kwargs)
-                                _ = func(env, version_def.msvc_version, vc_dir, '')
+                                _ = func(env, version_def.msvc_version, vc_dir)
 
                 for kwargs in [
-                    {'MSVC_SCRIPT_ARGS': '-vcvars_ver={}'.format(version_def.msvc_verstr)},
-                    {'MSVC_TOOLSET_VERSION': version_def.msvc_verstr},
+                    {'MSVC_SCRIPT_ARGS': '-vcvars_ver={}'.format(toolset_def.msvc_toolset_version)},
+                    {'MSVC_TOOLSET_VERSION': toolset_def.msvc_toolset_version},
                 ]:
                     env = Environment(**kwargs)
-                    _ = func(env, version_def.msvc_version, vc_dir, '')
+                    _ = func(env, version_def.msvc_version, vc_dir)
 
                 msvc_toolset_notfound_version = Data.msvc_toolset_notfound_version(version_def.msvc_version)
 
@@ -398,7 +413,7 @@ def run_msvc_script_args(self) -> None:
                 ]:
                     env = Environment(**kwargs)
                     with self.assertRaises(MSVCToolsetVersionNotFound):
-                        _ = func(env, version_def.msvc_version, vc_dir, '')
+                        _ = func(env, version_def.msvc_version, vc_dir)
 
                 msvc_sdk_notfound_version = Data.msvc_sdk_notfound_version(version_def.msvc_version)
 
@@ -407,15 +422,15 @@ def run_msvc_script_args(self) -> None:
                 ]:
                     env = Environment(**kwargs)
                     with self.assertRaises(MSVCSDKVersionNotFound):
-                        _ = func(env, version_def.msvc_version, vc_dir, '')
+                        _ = func(env, version_def.msvc_version, vc_dir)
 
                 have_spectre = toolset_def.msvc_toolset_version in Data.SPECTRE_TOOLSET_VERSIONS.get(version_def.msvc_version,[])
                 env = Environment(MSVC_SPECTRE_LIBS=True, MSVC_TOOLSET_VERSION=toolset_def.msvc_toolset_version)
                 if not have_spectre:
                     with self.assertRaises(MSVCSpectreLibsNotFound):
-                        _ = func(env, version_def.msvc_version, vc_dir, '')
+                        _ = func(env, version_def.msvc_version, vc_dir)
                 else:
-                    _ = func(env, version_def.msvc_version, vc_dir, '')
+                    _ = func(env, version_def.msvc_version, vc_dir)
 
                 msvc_sdk_version = Data.msvc_sdk_version(version_def.msvc_version)
 
@@ -443,14 +458,14 @@ def run_msvc_script_args(self) -> None:
                       }, (MSVCArgumentError, ),
                      ),
                     # multiple definitions
-                    ({'MSVC_TOOLSET_VERSION': version_def.msvc_verstr,
-                      'MSVC_SCRIPT_ARGS': "-vcvars_ver={}".format(version_def.msvc_verstr)
+                    ({'MSVC_TOOLSET_VERSION': toolset_def.msvc_toolset_version,
+                      'MSVC_SCRIPT_ARGS': "-vcvars_ver={}".format(toolset_def.msvc_toolset_version)
                       },
                      (MSVCArgumentError, ),
                      ),
                     # multiple definitions (args)
-                    ({'MSVC_TOOLSET_VERSION': version_def.msvc_verstr,
-                      'MSVC_SCRIPT_ARGS': "-vcvars_ver={0} undefined -vcvars_ver={0}".format(version_def.msvc_verstr)
+                    ({'MSVC_TOOLSET_VERSION': toolset_def.msvc_toolset_version,
+                      'MSVC_SCRIPT_ARGS': "-vcvars_ver={0} undefined -vcvars_ver={0}".format(toolset_def.msvc_toolset_version)
                       },
                      (MSVCArgumentError, ),
                      ),
@@ -484,7 +499,7 @@ def run_msvc_script_args(self) -> None:
                      (MSVCArgumentError, ),
                      ),
                     # toolset > msvc_version
-                    ({'MSVC_TOOLSET_VERSION': '{}.{}'.format(version_def.msvc_major, version_def.msvc_minor+1),
+                    ({'MSVC_TOOLSET_VERSION': '{}.{}'.format(latest_buildseries_major, latest_buildseries_minor+1),
                       },
                      (MSVCArgumentError, ),
                      ),
@@ -519,25 +534,90 @@ def run_msvc_script_args(self) -> None:
                 ] + more_tests:
                     env = Environment(**kwargs)
                     with self.assertRaises(exc_t):
-                        _ = func(env, version_def.msvc_version, vc_dir, '')
+                        _ = func(env, version_def.msvc_version, vc_dir)
 
             elif version_def.msvc_verstr == '14.0':
-                # VS2015: MSVC_SDK_VERSION and MSVC_UWP_APP
+
+                if msvc_version_is_express(version_def.msvc_version):
+                    sdk_supported = False
+                    uwp_supported = True  # based on target arch
+                elif msvc_version_is_btdispatch(version_def.msvc_version):
+                    sdk_supported = False
+                    uwp_supported = False
+                else:
+                    sdk_supported = True
+                    uwp_supported = True
 
                 env = Environment(MSVC_SCRIPT_ARGS='undefinedsymbol')
-                _ = func(env, version_def.msvc_version, vc_dir, '')
+                _ = func(env, version_def.msvc_version, vc_dir)
 
-                for msvc_uwp_app in (True, False):
+                if sdk_supported:
+                    # VS2015: MSVC_SDK_VERSION
+
+                    if uwp_supported:
+                        # VS2015: MSVC_UWP_APP
+
+                        for msvc_uwp_app in (True, False):
+
+                            sdk_list = WinSDK.get_msvc_sdk_version_list(version_def.msvc_version, msvc_uwp_app=msvc_uwp_app)
+                            for sdk_version in sdk_list:
+
+                                for kwargs in [
+                                    {'MSVC_SCRIPT_ARGS': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app},
+                                    {'MSVC_SDK_VERSION': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app},
+                                ]:
+                                    env = Environment(**kwargs)
+                                    _ = func(env, version_def.msvc_version, vc_dir)
+
+                    else:
+                        # VS2015: MSVC_UWP_APP error
+
+                        for msvc_uwp_app in (True,):
+
+                            sdk_list = WinSDK.get_msvc_sdk_version_list(version_def.msvc_version, msvc_uwp_app=msvc_uwp_app)
+                            for sdk_version in sdk_list:
+
+                                for kwargs in [
+                                    {'MSVC_SCRIPT_ARGS': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app},
+                                    {'MSVC_SDK_VERSION': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app},
+                                ]:
+                                    env = Environment(**kwargs)
+                                    with self.assertRaises(MSVCArgumentError):
+                                        _ = func(env, version_def.msvc_version, vc_dir)
+
+                else:
+                    # VS2015: MSVC_SDK_VERSION error
 
                     sdk_list = WinSDK.get_msvc_sdk_version_list(version_def.msvc_version, msvc_uwp_app=msvc_uwp_app)
                     for sdk_version in sdk_list:
 
-                        for kwargs in [
-                            {'MSVC_SCRIPT_ARGS': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app},
-                            {'MSVC_SDK_VERSION': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app},
-                        ]:
-                            env = Environment(**kwargs)
-                            _ = func(env, version_def.msvc_version, vc_dir, '')
+                        env = Environment(MSVC_SDK_VERSION=sdk_version)
+                        with self.assertRaises(MSVCArgumentError):
+                            _ = func(env, version_def.msvc_version, vc_dir)
+
+                        # MSVC_SCRIPT_ARGS sdk_version not validated
+                        env = Environment(MSVC_SCRIPT_ARGS=sdk_version)
+                        _ = func(env, version_def.msvc_version, vc_dir)
+
+                    if uwp_supported:
+                        # VS2015: MSVC_UWP_APP
+
+                        for msvc_uwp_app in (True, False):
+                            env = Environment(MSVC_UWP_APP=msvc_uwp_app)
+                            _ = func(env, version_def.msvc_version, vc_dir)
+
+                    else:
+                        # VS2015: MSVC_UWP_APP error
+
+                        for msvc_uwp_app in (True,):
+
+                            env = Environment(MSVC_UWP_APP=msvc_uwp_app)
+                            with self.assertRaises(MSVCArgumentError):
+                                _ = func(env, version_def.msvc_version, vc_dir)
+
+                # MSVC_SCRIPT_ARGS store not validated
+                env = Environment(MSVC_SCRIPT_ARGS='store')
+                _ = func(env, version_def.msvc_version, vc_dir)
 
                 for kwargs in [
                     {'MSVC_SPECTRE_LIBS': True, 'MSVC_SCRIPT_ARGS': None},
@@ -545,14 +625,14 @@ def run_msvc_script_args(self) -> None:
                 ]:
                     env = Environment(**kwargs)
                     with self.assertRaises(MSVCArgumentError):
-                        _ = func(env, version_def.msvc_version, vc_dir, '')
+                        _ = func(env, version_def.msvc_version, vc_dir)
 
             else:
                 # VS2013 and earlier: no arguments
 
                 env = Environment(MSVC_SCRIPT_ARGS='undefinedsymbol')
                 with self.assertRaises(MSVCArgumentError):
-                    _ = func(env, version_def.msvc_version, vc_dir, '')
+                    _ = func(env, version_def.msvc_version, vc_dir)
 
                 for kwargs in [
                     {'MSVC_UWP_APP': True, 'MSVC_SCRIPT_ARGS': None},
@@ -563,7 +643,7 @@ def run_msvc_script_args(self) -> None:
                 ]:
                     env = Environment(**kwargs)
                     with self.assertRaises(MSVCArgumentError):
-                        _ = func(env, version_def.msvc_version, vc_dir, '')
+                        _ = func(env, version_def.msvc_version, vc_dir)
 
     def test_msvc_script_args_none(self) -> None:
         force = ScriptArguments.msvc_force_default_arguments(force=False)
diff --git a/SCons/Tool/MSCommon/MSVC/Util.py b/SCons/Tool/MSCommon/MSVC/Util.py
index 6fd188bc35..0da58e1e4d 100644
--- a/SCons/Tool/MSCommon/MSVC/Util.py
+++ b/SCons/Tool/MSCommon/MSVC/Util.py
@@ -37,6 +37,15 @@
 
 from . import Config
 
+
+# call _initialize method upon class definition completion
+
+class AutoInitialize:
+    def __init_subclass__(cls, **kwargs):
+        super().__init_subclass__(**kwargs)
+        if hasattr(cls, '_initialize') and callable(getattr(cls, '_initialize', None)):
+            cls._initialize()
+
 # path utilities
 
 # windows drive specification (e.g., 'C:')
@@ -234,6 +243,26 @@ def get_msvc_version_prefix(version):
             rval = m.group('version')
     return rval
 
+def get_msvc_version_prefix_suffix(version):
+    """
+    Get the msvc version number prefix and suffix from a string.
+
+    Args:
+        version: str
+            version specification
+
+    Returns:
+        (str, str): the msvc version prefix and suffix
+
+    """
+    prefix = suffix = ''
+    if version:
+        m = re_msvc_version.match(version)
+        if m:
+            prefix = m.group('msvc_version')
+            suffix = m.group('suffix') if m.group('suffix') else ''
+    return prefix, suffix
+
 # toolset version query utilities
 
 def is_toolset_full(toolset_version) -> bool:
@@ -322,15 +351,21 @@ def msvc_version_components(vcver):
     return msvc_version_components_def
 
 _MSVC_EXTENDED_VERSION_COMPONENTS_DEFINITION = namedtuple('MSVCExtendedVersionComponentsDefinition', [
-    'msvc_version', # msvc version (e.g., '14.1Exp')
-    'msvc_verstr',  # msvc version numeric string (e.g., '14.1')
-    'msvc_suffix',  # msvc version component type (e.g., 'Exp')
-    'msvc_vernum',  # msvc version floating point number (e.g, 14.1)
-    'msvc_major',   # msvc major version integer number (e.g., 14)
-    'msvc_minor',   # msvc minor version integer number (e.g., 1)
-    'msvc_comps',   # msvc version components tuple (e.g., ('14', '1'))
+    'msvc_version',      # msvc version (e.g., '14.1Exp')
+    'msvc_verstr',       # msvc version numeric string (e.g., '14.1')
+    'msvc_suffix',       # msvc version component type (e.g., 'Exp')
+    'msvc_suffix_rank',  # msvc version component rank (0, 1)
+    'msvc_vernum',       # msvc version floating point number (e.g, 14.1)
+    'msvc_major',        # msvc major version integer number (e.g., 14)
+    'msvc_minor',        # msvc minor version integer number (e.g., 1)
+    'msvc_comps',        # msvc version components tuple (e.g., ('14', '1'))
+    'msvc_buildtools',       # msvc build tools
+    'msvc_buildtools_num',   # msvc build tools integer number
+    'msvc_buildseries',      # msvc build series
+    'msvc_buildseries_num',  # msvc build series floating point number
     'msvc_toolset_version',  # msvc toolset version
     'msvc_toolset_comps',    # msvc toolset version components
+    'msvc_toolset_is_sxs',   # msvc toolset version is sxs
     'version',               # msvc version or msvc toolset version
 ])
 
@@ -355,11 +390,19 @@ def msvc_extended_version_components(version):
 
     msvc_toolset_version = m.group('version')
     msvc_toolset_comps = tuple(msvc_toolset_version.split('.'))
+    msvc_toolset_is_sxs = is_toolset_sxs(msvc_toolset_version)
 
-    msvc_verstr = get_msvc_version_prefix(msvc_toolset_version)
-    if not msvc_verstr:
+    vc_verstr = get_msvc_version_prefix(msvc_toolset_version)
+    if not vc_verstr:
         return None
 
+    vc_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL.get(vc_verstr)
+    if not vc_buildseries_def:
+        return None
+
+    vc_buildtools_def = Config.VC_BUILDTOOLS_MAP[vc_buildseries_def.vc_buildseries]
+
+    msvc_verstr = vc_buildtools_def.msvc_version
     msvc_suffix = m.group('suffix') if m.group('suffix') else ''
     msvc_version = msvc_verstr + msvc_suffix
 
@@ -376,12 +419,18 @@ def msvc_extended_version_components(version):
         msvc_version = msvc_version,
         msvc_verstr = msvc_verstr,
         msvc_suffix = msvc_suffix,
+        msvc_suffix_rank = 0 if not msvc_suffix else 1,
         msvc_vernum = msvc_vernum,
         msvc_major = msvc_major,
         msvc_minor = msvc_minor,
         msvc_comps = msvc_comps,
+        msvc_buildtools = vc_buildtools_def.msvc_version,
+        msvc_buildtools_num = vc_buildtools_def.msvc_version_numeric,
+        msvc_buildseries = vc_buildseries_def.vc_version,
+        msvc_buildseries_num = vc_buildseries_def.vc_version_numeric,
         msvc_toolset_version = msvc_toolset_version,
         msvc_toolset_comps = msvc_toolset_comps,
+        msvc_toolset_is_sxs = msvc_toolset_is_sxs,
         version = version,
     )
 
diff --git a/SCons/Tool/MSCommon/MSVC/__init__.py b/SCons/Tool/MSCommon/MSVC/__init__.py
index 766894d9bb..f87b0f1d8c 100644
--- a/SCons/Tool/MSCommon/MSVC/__init__.py
+++ b/SCons/Tool/MSCommon/MSVC/__init__.py
@@ -40,6 +40,7 @@
 from . import Config  # noqa: F401
 from . import Util  # noqa: F401
 from . import Registry  # noqa: F401
+from . import Kind  # noqa: F401
 from . import SetupEnvDefault  # noqa: F401
 from . import Policy  # noqa: F401
 from . import WinSDK  # noqa: F401
diff --git a/SCons/Tool/MSCommon/README.rst b/SCons/Tool/MSCommon/README.rst
index 5ab07ad631..b37f409d3d 100644
--- a/SCons/Tool/MSCommon/README.rst
+++ b/SCons/Tool/MSCommon/README.rst
@@ -27,16 +27,176 @@ Design Notes
   ``MSCommon/vc.py`` and are available via the ``SCons.Tool.MSCommon`` namespace.
 
 
+MSVC Detection Priority
+=======================
+
+For msvc version specifications without an 'Exp' suffix, an express
+installation is used only when no other installation is detected.
+
++---------+---------+----------------------------------------------------------+
+| Product | VCVer   | Priority                                                 |
++=========+=========+==========================================================+
+| VS2022  | 14.3    | Enterprise, Professional, Community, BuildTools          |
++---------+---------+----------------------------------------------------------+
+| VS2019  | 14.2    | Enterprise, Professional, Community, BuildTools          |
++---------+---------+----------------------------------------------------------+
+| VS2017  | 14.1    | Enterprise, Professional, Community, BuildTools, Express |
++---------+---------+----------------------------------------------------------+
+| VS2017  | 14.1Exp | Express                                                  |
++---------+---------+----------------------------------------------------------+
+| VS2015  | 14.0    | [Develop, BuildTools, CmdLine], Express                  |
++---------+---------+----------------------------------------------------------+
+| VS2015  | 14.0Exp | Express                                                  |
++---------+---------+----------------------------------------------------------+
+| VS2013  | 12.0    | Develop, Express                                         |
++---------+---------+----------------------------------------------------------+
+| VS2013  | 12.0Exp | Express                                                  |
++---------+---------+----------------------------------------------------------+
+| VS2012  | 11.0    | Develop, Express                                         |
++---------+---------+----------------------------------------------------------+
+| VS2012  | 11.0Exp | Express                                                  |
++---------+---------+----------------------------------------------------------+
+| VS2010  | 10.0    | Develop, Express                                         |
++---------+---------+----------------------------------------------------------+
+| VS2010  | 10.0Exp | Express                                                  |
++---------+---------+----------------------------------------------------------+
+| VS2008  | 9.0     | Develop, VCForPython, Express                            |
++---------+---------+----------------------------------------------------------+
+| VS2008  | 9.0Exp  | Express                                                  |
++---------+---------+----------------------------------------------------------+
+| VS2005  | 8.0     | Develop, Express                                         |
++---------+---------+----------------------------------------------------------+
+| VS2005  | 8.0Exp  | Express                                                  |
++---------+---------+----------------------------------------------------------+
+| VS2003  | 7.1     | Develop                                                  |
++---------+---------+----------------------------------------------------------+
+| VS2002  | 7.0     | Develop                                                  |
++---------+---------+----------------------------------------------------------+
+| VS6.0   | 6.0     | Develop                                                  |
++---------+---------+----------------------------------------------------------+
+
+Legend:
+
+  Develop
+    devenv.com or msdev.com is detected.
+  
+  Express
+    WDExpress.exe or VCExpress.exe is detected.
+  
+  BuildTools [VS2015]
+    The vcvarsall batch file dispatches to the buildtools batch file.
+  
+  CmdLine [VS2015]
+    Neither Develop, Express, or BuildTools.
+
+VS2015 Edition Limitations
+==========================
+
+VS2015 BuildTools
+-----------------
+
+The VS2015 BuildTools stand-alone batch file does not support the ``sdk version`` argument.
+
+The VS2015 BuildTools stand-alone batch file does not support the ``store`` argument.
+
+These arguments appear to be silently ignored and likely would result in compiler
+and/or linker build failures.
+
+The VS2015 BuildTools ``vcvarsall.bat`` batch file dispatches to the stand-alone buildtools
+batch file under certain circumstances. A fragment from the vcvarsall batch file is:
+
+::
+
+    if exist "%~dp0..\common7\IDE\devenv.exe" goto setup_VS
+    if exist "%~dp0..\common7\IDE\wdexpress.exe" goto setup_VS
+    if exist "%~dp0..\..\Microsoft Visual C++ Build Tools\vcbuildtools.bat" goto setup_buildsku
+
+    :setup_VS
+
+    ...
+
+    :setup_buildsku
+    if not exist "%~dp0..\..\Microsoft Visual C++ Build Tools\vcbuildtools.bat" goto usage
+    set CurrentDir=%CD%
+    call "%~dp0..\..\Microsoft Visual C++ Build Tools\vcbuildtools.bat" %1 %2
+    cd /d %CurrentDir%
+    goto :eof
+
+VS2015 Express
+--------------
+
+The VS2015 Express batch file does not support the ``sdk version`` argument.
+
+The VS2015 Express batch file does not support the ``store`` argument for the ``amd64`` and
+``arm`` target architectures
+
+amd64 Target Architecture
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+As installed, VS2015 Express does not support the ``store`` argument for the ``amd64`` target
+architecture.  The generated ``store`` library paths include directories that do not exist.
+
+The store library paths appear in two places in the ``vcvarsx86_amd64`` batch file:
+
+::
+
+    :setstorelib
+    @if exist "%VCINSTALLDIR%LIB\amd64\store" set LIB=%VCINSTALLDIR%LIB\amd64\store;%LIB%
+    ...
+    :setstorelibpath
+    @if exist "%VCINSTALLDIR%LIB\amd64\store" set LIBPATH=%VCINSTALLDIR%LIB\amd64\store;%LIBPATH%
+
+The correct store library paths would be:
+
+::
+
+    :setstorelib
+    @if exist "%VCINSTALLDIR%LIB\store\amd64" set LIB=%VCINSTALLDIR%LIB\store\amd64;%LIB%
+    ...
+    :setstorelibpath
+    @if exist "%VCINSTALLDIR%LIB\store\amd64" set LIBPATH=%VCINSTALLDIR%LIB\store\amd64;%LIBPATH%
+
+arm Target Architecture
+^^^^^^^^^^^^^^^^^^^^^^^
+
+As installed, VS2015 Express does not support the ``store`` argument for the ``arm`` target
+architecture.  The generated ``store`` library paths include directories that do not exist.
+
+The store library paths appear in two places in the ``vcvarsx86_arm`` batch file:
+
+::
+
+    :setstorelib
+    @if exist "%VCINSTALLDIR%LIB\ARM\store" set LIB=%VCINSTALLDIR%LIB\ARM\store;%LIB%
+    ...
+    :setstorelibpath
+    @if exist "%VCINSTALLDIR%LIB\ARM\store" set LIBPATH=%VCINSTALLDIR%LIB\ARM\store;%LIBPATH%
+
+The correct store library paths would be file:
+
+::
+
+    :setstorelib
+    @if exist "%VCINSTALLDIR%LIB\store\ARM" set LIB=%VCINSTALLDIR%LIB\store\ARM;%LIB%
+    ...
+    :setstorelibpath
+    @if exist "%VCINSTALLDIR%LIB\store\ARM" set LIBPATH=%VCINSTALLDIR%LIB\store\ARM;%LIBPATH%
+
+
 Known Issues
 ============
 
 The following issues are known to exist:
 
 * Using ``MSVC_USE_SCRIPT`` and ``MSVC_USE_SCRIPT_ARGS`` to call older Microsoft SDK
-  ``SetEnv.cmd`` batch files may result in build failures.  Some of these batch files
-  require delayed expansion to be enabled which is not usually the Windows default.
-  One solution would be to launch the MSVC batch file command in a new command interpreter
-  instance with delayed expansion enabled via command-line options.
+  ``SetEnv.cmd`` batch files may result in build failures.
+
+  Typically, the reasons for build failures with SDK batch files are one, or both, of:
+
+  * The batch files require delayed expansion to be enabled which is not usually the Windows default.
+
+  * The batch files inspect environment variables that are not defined in the minimal subprocess
+    environment in which the batch files are invoked.
 
 * The code to suppress the "No versions of the MSVC compiler were found" warning for
   the default environment was moved from ``MSCommon/vc.py`` to ``MSCommon/MSVC/SetupEnvDefault.py``.
@@ -47,20 +207,24 @@ The following issues are known to exist:
 Experimental Features
 =====================
 
-msvc_query_version_toolset(version=None, prefer_newest=True)
-------------------------------------------------------------
+msvc_query_version_toolset(version=None, prefer_newest=True, vswhere_exe=None)
+------------------------------------------------------------------------------
 
 The experimental function ``msvc_query_version_toolset`` was added to ``MSCommon/vc.py``
-and is available via the ``SCons.Tool.MSCommon`` namespace. This function takes a version
-specification or a toolset version specification and a product preference as arguments and
-returns the msvc version and the msvc toolset version for the corresponding version specification.
+and is available via the ``SCons.Tool.MSCommon`` namespace.
+
+This function takes a version specification or a toolset version specification, an optional product
+preference, and an optional vswhere executable location as arguments and returns the msvc version and
+the msvc toolset version for the corresponding version specification.
 
 This is a proxy for using the toolset version for selection until that functionality can be added.
 
 Example usage:
+
 ::
 
     for version in [
+        '14.4',
         '14.3',
         '14.2',
         '14.1',
@@ -90,6 +254,7 @@ Example usage:
             print('{}Query: {} version={}, prefer_newest={}'.format(newline, msg, version, prefer_newest))
 
 Example output fragment
+
 ::
 
     Build: _build003 {'MSVC_VERSION': '14.3', 'MSVC_TOOLSET_VERSION': '14.29.30133'}
@@ -117,6 +282,7 @@ added to the batch file argument list.  This is intended to make the cache more
 updates that may change the default toolset version and/or the default SDK version.
 
 Example usage:
+
 ::
 
     @echo Enabling scons cache ...
@@ -139,6 +305,7 @@ Enabling warnings to be produced for detected msvc batch file errors may provide
 for build failures. Refer to the documentation for details.
 
 Change the default policy:
+
 ::
 
     from SCons.Tool.MSCommon import msvc_set_scripterror_policy
@@ -146,6 +313,7 @@ Change the default policy:
     msvc_set_scripterror_policy('Warning')
 
 Specify the policy per-environment:
+
 ::
 
     env = Environment(MSVC_VERSION='14.3', MSVC_SPECTRE_LIBS=True, MSVC_SCRIPTERROR_POLICY='Warning')
@@ -171,6 +339,7 @@ On occasion, the raw vswhere output may prove useful especially if there are sus
 detection of installed msvc instances.
 
 Windows command-line sample invocations:
+
 ::
 
     @rem 64-Bit Windows
@@ -188,31 +357,37 @@ Batch File Arguments
 
 Supported MSVC batch file arguments by product:
 
-======= === === ======= =======
-Product UWP SDK Toolset Spectre
-======= === === ======= =======
-VS2022   X   X     X       X
-------- --- --- ------- -------
-VS2019   X   X     X       X
-------- --- --- ------- -------
-VS2017   X   X     X       X
-------- --- --- ------- -------
-VS2015   X   X
-======= === === ======= =======
++---------+---------+--------+---------+---------+
+| Product | UWP     | SDK    | Toolset | Spectre |
++=========+=========+========+=========+=========+
+| VS2022  | X       | X      | X       | X       |
++---------+---------+--------+---------+---------+
+| VS2019  | X       | X      | X       | X       |
++---------+---------+--------+---------+---------+
+| VS2017  | X       | X      | X       | X       |
++---------+---------+--------+---------+---------+
+| VS2015  | X [1]   | X [2]  |         |         |
++---------+---------+--------+---------+---------+
+
+Notes:
+
+1) The BuildTools edition does not support the ``store`` argument.  The Express edition
+   supports the ``store`` argument for the ``x86`` target only.
+2) The ``sdk version`` argument is not supported in the BuildTools and Express editions.
 
 Supported MSVC batch file arguments in SCons:
 
-======== ====================================== ===================================================
-Argument Construction Variable                  Script Argument Equivalent
-======== ====================================== ===================================================
-UWP      ``MSVC_UWP_APP=True``                  ``MSVC_SCRIPT_ARGS='store'``
--------- -------------------------------------- ---------------------------------------------------
-SDK      ``MSVC_SDK_VERSION='10.0.20348.0'``    ``MSVC_SCRIPT_ARGS='10.0.20348.0'``
--------- -------------------------------------- ---------------------------------------------------
-Toolset  ``MSVC_TOOLSET_VERSION='14.31.31103'`` ``MSVC_SCRIPT_ARGS='-vcvars_ver=14.31.31103'``
--------- -------------------------------------- ---------------------------------------------------
-Spectre  ``MSVC_SPECTRE_LIBS=True``             ``MSVC_SCRIPT_ARGS='-vcvars_spectre_libs=spectre'``
-======== ====================================== ===================================================
++----------+----------------------------------------+-----------------------------------------------------+
+| Argument | Construction Variable                  | Script Argument Equivalent                          |
++==========+========================================+=====================================================+
+| UWP      | ``MSVC_UWP_APP=True``                  | ``MSVC_SCRIPT_ARGS='store'``                        |
++----------+----------------------------------------+-----------------------------------------------------+
+| SDK      | ``MSVC_SDK_VERSION='10.0.20348.0'``    | ``MSVC_SCRIPT_ARGS='10.0.20348.0'``                 |
++----------+----------------------------------------+-----------------------------------------------------+
+| Toolset  | ``MSVC_TOOLSET_VERSION='14.31.31103'`` | ``MSVC_SCRIPT_ARGS='-vcvars_ver=14.31.31103'``      |
++----------+----------------------------------------+-----------------------------------------------------+
+| Spectre  | ``MSVC_SPECTRE_LIBS=True``             | ``MSVC_SCRIPT_ARGS='-vcvars_spectre_libs=spectre'`` |
++----------+----------------------------------------+-----------------------------------------------------+
 
 **MSVC_SCRIPT_ARGS contents are not validated.  Utilizing script arguments that have construction
 variable equivalents is discouraged and may lead to difficult to diagnose build errors.**
@@ -244,6 +419,7 @@ that the msvc batch files would return.  When using ``MSVC_SCRIPT_ARGS``, the
 toolset specification should be omitted entirely.
 
 Local installation and summary test results:
+
 ::
 
     VS2022\VC\Auxiliary\Build\Microsoft.VCToolsVersion.v143.default.txt
@@ -253,6 +429,7 @@ Local installation and summary test results:
         14.32.31326
 
 Toolset version summary:
+
 ::
 
     14.31.31103   Environment()
@@ -268,6 +445,7 @@ Toolset version summary:
     14.32.31326   Environment(MSVC_SCRIPT_ARGS=['-vcvars_ver=14.32'])
 
 VS2022\\Common7\\Tools\\vsdevcmd\\ext\\vcvars.bat usage fragment:
+
 ::
 
     @echo     -vcvars_ver=version : Version of VC++ Toolset to select
@@ -289,6 +467,7 @@ VS2022\\Common7\\Tools\\vsdevcmd\\ext\\vcvars.bat usage fragment:
     @echo                             SxS toolset to [VSInstallDir]\VC\MSVC\Tools\ directory.
 
 VS2022 batch file fragment to determine the default toolset version:
+
 ::
 
     @REM Add MSVC
@@ -315,75 +494,114 @@ Visual Studio Version Notes
 SDK Versions
 ------------
 
-==== ============
-SDK  Format
-==== ============
-10.0 10.0.XXXXX.Y
----- ------------
-8.1  8.1
-==== ============
++------+-------------------+
+| SDK  | Format            |
++======+===================+
+| 10.0 | 10.0.XXXXX.Y [1]  |
++------+-------------------+
+| 8.1  | 8.1               |
++------+-------------------+
+
+Notes:
+
+1) The Windows 10 SDK version number is 10.0.20348.0 and earlier.
+
+   The Windows 11 SDK version number is 10.0.22000.194 and later.
+
+BuildSeries Versions
+--------------------
+
++-------------+-------+-------+
+| BuildSeries | VCVER | CLVER |
++=============+=======+=======+
+| 14.4        | 14.4X | 19.4  |
++-------------+-------+-------+
+| 14.3        | 14.3X | 19.3  |
++-------------+-------+-------+
+| 14.2        | 14.2X | 19.2  |
++-------------+-------+-------+
+| 14.1        | 14.1X | 19.1  |
++-------------+-------+-------+
+| 14.0        | 14.0  | 19.0  |
++-------------+-------+-------+
+| 12.0        | 12.0  | 18.0  |
++-------------+-------+-------+
+| 11.0        | 11.0  | 17.0  |
++-------------+-------+-------+
+| 10.0        | 10.0  | 16.0  |
++-------------+-------+-------+
+| 9.0         | 9.0   | 15.0  |
++-------------+-------+-------+
+| 8.0         | 8.0   | 14.0  |
++-------------+-------+-------+
+| 7.1         | 7.1   | 13.1  |
++-------------+-------+-------+
+| 7.0         | 7.0   | 13.0  |
++-------------+-------+-------+
+| 6.0         | 6.0   | 12.0  |
++-------------+-------+-------+
 
 BuildTools Versions
 -------------------
 
-========== ===== ===== ========
-BuildTools VCVER CLVER MSVCRT
-========== ===== ===== ========
-v143       14.3   19.3 140/ucrt
----------- ----- ----- --------
-v142       14.2   19.2 140/ucrt
----------- ----- ----- --------
-v141       14.1   19.1 140/ucrt
----------- ----- ----- --------
-v140       14.0   19.0 140/ucrt
----------- ----- ----- --------
-v120       12.0   18.0 120
----------- ----- ----- --------
-v110       11.0   17.0 110
----------- ----- ----- --------
-v100       10.0   16.0 100
----------- ----- ----- --------
-v90         9.0   15.0  90
----------- ----- ----- --------
-v80         8.0   14.0  80
----------- ----- ----- --------
-v71         7.1   13.1  71
----------- ----- ----- --------
-v70         7.0   13.0  70
----------- ----- ----- --------
-v60         6.0   12.0  60
-========== ===== ===== ========
++------------+-------------+----------+
+| BuildTools | BuildSeries | MSVCRT   |
++============+=============+==========+
+| v143       | 14.4, 14.3  | 140/ucrt |
++------------+-------------+----------+
+| v142       | 14.2        | 140/ucrt |
++------------+-------------+----------+
+| v141       | 14.1        | 140/ucrt |
++------------+-------------+----------+
+| v140       | 14.0        | 140/ucrt |
++------------+-------------+----------+
+| v120       | 12.0        | 120      |
++------------+-------------+----------+
+| v110       | 11.0        | 110      |
++------------+-------------+----------+
+| v100       | 10.0        | 100      |
++------------+-------------+----------+
+| v90        | 9.0         | 90       |
++------------+-------------+----------+
+| v80        | 8.0         | 80       |
++------------+-------------+----------+
+| v71        | 7.1         | 71       |
++------------+-------------+----------+
+| v70        | 7.0         | 70       |
++------------+-------------+----------+
+| v60        | 6.0         | 60       |
++------------+-------------+----------+
 
 Product Versions
 ----------------
 
-======== ===== ========= ======================
-Product  VSVER SDK       BuildTools
-======== ===== ========= ======================
-2022     17.0  10.0, 8.1 v143, v142, v141, v140
--------- ----- --------- ----------------------
-2019     16.0  10.0, 8.1 v142, v141, v140
--------- ----- --------- ----------------------
-2017     15.0  10.0, 8.1 v141, v140
--------- ----- --------- ----------------------
-2015     14.0  10.0, 8.1 v140
--------- ----- --------- ----------------------
-2013     12.0            v120
--------- ----- --------- ----------------------
-2012     11.0            v110
--------- ----- --------- ----------------------
-2010     10.0            v100
--------- ----- --------- ----------------------
-2008      9.0            v90
--------- ----- --------- ----------------------
-2005      8.0            v80
--------- ----- --------- ----------------------
-2003.NET  7.1            v71
--------- ----- --------- ----------------------
-2002.NET  7.0            v70
--------- ----- --------- ----------------------
-6.0       6.0            v60
-======== ===== ========= ======================
++----------+-------+-------+-----------+------------------------+
+| Product  | VSVER | SCons | SDK       | BuildTools             |
++==========+=======+=======+===========+========================+
+| 2022     | 17.0  | 14.3  | 10.0, 8.1 | v143, v142, v141, v140 |
++----------+-------+-------+-----------+------------------------+
+| 2019     | 16.0  | 14.2  | 10.0, 8.1 | v142, v141, v140       |
++----------+-------+-------+-----------+------------------------+
+| 2017     | 15.0  | 14.1  | 10.0, 8.1 | v141, v140             |
++----------+-------+-------+-----------+------------------------+
+| 2015     | 14.0  | 14.0  | 10.0, 8.1 | v140                   |
++----------+-------+-------+-----------+------------------------+
+| 2013     | 12.0  | 12.0  |           | v120                   |
++----------+-------+-------+-----------+------------------------+
+| 2012     | 11.0  | 11.0  |           | v110                   |
++----------+-------+-------+-----------+------------------------+
+| 2010     | 10.0  | 10.0  |           | v100                   |
++----------+-------+-------+-----------+------------------------+
+| 2008     | 9.0   | 9.0   |           | v90                    |
++----------+-------+-------+-----------+------------------------+
+| 2005     | 8.0   | 8.0   |           | v80                    |
++----------+-------+-------+-----------+------------------------+
+| 2003.NET | 7.1   | 7.1   |           | v71                    |
++----------+-------+-------+-----------+------------------------+
+| 2002.NET | 7.0   | 7.0   |           | v70                    |
++----------+-------+-------+-----------+------------------------+
+| 6.0      | 6.0   | 6.0   |           | v60                    |
++----------+-------+-------+-----------+------------------------+
 
 
 SCons Implementation Notes
diff --git a/SCons/Tool/MSCommon/__init__.py b/SCons/Tool/MSCommon/__init__.py
index c3078ac630..63f9ae4de6 100644
--- a/SCons/Tool/MSCommon/__init__.py
+++ b/SCons/Tool/MSCommon/__init__.py
@@ -45,6 +45,9 @@
     msvc_toolset_versions,
     msvc_toolset_versions_spectre,
     msvc_query_version_toolset,
+    vswhere_register_executable,
+    vswhere_get_executable,
+    vswhere_freeze_executable,
 )
 
 from SCons.Tool.MSCommon.vs import (  # noqa: F401
@@ -60,6 +63,8 @@
     msvc_get_notfound_policy,
     msvc_set_scripterror_policy,
     msvc_get_scripterror_policy,
+    msvc_notfound_policy_contextmanager,
+    msvc_scripterror_policy_contextmanager,
 )
 
 from .MSVC.Exceptions import (  # noqa: F401
@@ -78,7 +83,9 @@
     MSVCUnsupportedHostArch,
     MSVCUnsupportedTargetArch,
     MSVCScriptNotFound,
+    MSVCUseScriptError,
     MSVCUseSettingsError,
+    VSWhereUserError,
 )
 
 from .MSVC.Util import (  # noqa: F401
diff --git a/SCons/Tool/MSCommon/sdk.py b/SCons/Tool/MSCommon/sdk.py
index c6600f3a5b..a06dfacfc6 100644
--- a/SCons/Tool/MSCommon/sdk.py
+++ b/SCons/Tool/MSCommon/sdk.py
@@ -372,7 +372,7 @@ def mssdk_setup_env(env):
             return
         msvs_version = env.subst(msvs_version)
         from . import vs
-        msvs = vs.get_vs_by_version(msvs_version)
+        msvs = vs.get_vs_by_version(msvs_version, env)
         debug('mssdk_setup_env:msvs is :%s', msvs)
         if not msvs:
             debug('mssdk_setup_env: no VS version detected, bailingout:%s', msvs)
diff --git a/SCons/Tool/MSCommon/vc.py b/SCons/Tool/MSCommon/vc.py
index e43c618cbd..032dbf202e 100644
--- a/SCons/Tool/MSCommon/vc.py
+++ b/SCons/Tool/MSCommon/vc.py
@@ -25,9 +25,6 @@
 MS Compilers: Visual C/C++ detection and configuration.
 
 # TODO:
-#   * gather all the information from a single vswhere call instead
-#     of calling repeatedly (use json format?)
-#   * support passing/setting location for vswhere in env.
 #   * supported arch for versions: for old versions of batch file without
 #     argument, giving bogus argument cannot be detected, so we have to hardcode
 #     this here
@@ -52,19 +49,26 @@
     namedtuple,
     OrderedDict,
 )
+import json
+from functools import cmp_to_key
+import enum
 
 import SCons.Util
 import SCons.Warnings
 from SCons.Tool import find_program_path
 
 from . import common
-from .common import CONFIG_CACHE, debug
+from .common import (
+    CONFIG_CACHE,
+    debug,
+)
 from .sdk import get_installed_sdks
 
 from . import MSVC
 
 from .MSVC.Exceptions import (
     VisualCException,
+    MSVCInternalError,
     MSVCUserError,
     MSVCArgumentError,
     MSVCToolsetVersionNotFound,
@@ -81,9 +85,15 @@ class MSVCUnsupportedTargetArch(VisualCException):
 class MSVCScriptNotFound(MSVCUserError):
     pass
 
+class MSVCUseScriptError(MSVCUserError):
+    pass
+
 class MSVCUseSettingsError(MSVCUserError):
     pass
 
+class VSWhereUserError(MSVCUserError):
+    pass
+
 # internal exceptions
 
 class UnsupportedVersion(VisualCException):
@@ -102,8 +112,8 @@ class BatchFileExecutionError(VisualCException):
 
 # MSVC 9.0 preferred query order:
 #     True:  VCForPython, VisualStudio
-#     FAlse: VisualStudio, VCForPython
-_VC90_Prefer_VCForPython = True
+#     False: VisualStudio, VCForPython
+_VC90_Prefer_VCForPython = False
 
 # Dict to 'canonalize' the arch
 _ARCH_TO_CANONICAL = {
@@ -393,7 +403,7 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
 
 # debug("_LE2019_HOST_TARGET_CFG: %s", _LE2019_HOST_TARGET_CFG)
 
-# 14.0 (VS2015) to 8.0 (VS2005)
+# 14.0 (VS2015) to 10.0 (VS2010)
 
 # Given a (host, target) tuple, return a tuple containing the argument for
 # the batch file and a tuple of the path components to find cl.exe.
@@ -402,23 +412,23 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
 # bin directory (i.e., <VSROOT>/VC/bin).  Any other tools are in subdirectory
 # named for the the host/target pair or a single name if the host==target.
 
-_LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS = {
+_LE2015_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS = {
 
-    ('amd64', 'amd64') : ('amd64',     ('bin', 'amd64')),
-    ('amd64', 'x86')   : ('amd64_x86', ('bin', 'amd64_x86')),
-    ('amd64', 'arm')   : ('amd64_arm', ('bin', 'amd64_arm')),
+    ('amd64', 'amd64') : ('amd64',     'vcvars64.bat',         ('bin', 'amd64')),
+    ('amd64', 'x86')   : ('amd64_x86', 'vcvarsamd64_x86.bat',  ('bin', 'amd64_x86')),
+    ('amd64', 'arm')   : ('amd64_arm', 'vcvarsamd64_arm.bat',  ('bin', 'amd64_arm')),
 
-    ('x86',   'amd64') : ('x86_amd64', ('bin', 'x86_amd64')),
-    ('x86',   'x86')   : ('x86',       ('bin', )),
-    ('x86',   'arm')   : ('x86_arm',   ('bin', 'x86_arm')),
-    ('x86',   'ia64')  : ('x86_ia64',  ('bin', 'x86_ia64')),
+    ('x86',   'amd64') : ('x86_amd64', 'vcvarsx86_amd64.bat',  ('bin', 'x86_amd64')),
+    ('x86',   'x86')   : ('x86',       'vcvars32.bat',         ('bin', )),
+    ('x86',   'arm')   : ('x86_arm',   'vcvarsx86_arm.bat',    ('bin', 'x86_arm')),
+    ('x86',   'ia64')  : ('x86_ia64',  'vcvarsx86_ia64.bat',   ('bin', 'x86_ia64')),
 
-    ('arm64', 'amd64') : ('amd64',     ('bin', 'amd64')),
-    ('arm64', 'x86')   : ('amd64_x86', ('bin', 'amd64_x86')),
-    ('arm64', 'arm')   : ('amd64_arm', ('bin', 'amd64_arm')),
+    ('arm64', 'amd64') : ('amd64',     'vcvars64.bat',         ('bin', 'amd64')),
+    ('arm64', 'x86')   : ('amd64_x86', 'vcvarsamd64_x86.bat',  ('bin', 'amd64_x86')),
+    ('arm64', 'arm')   : ('amd64_arm', 'vcvarsamd64_arm.bat',  ('bin', 'amd64_arm')),
 
-    ('arm',   'arm')   : ('arm',       ('bin', 'arm')),
-    ('ia64',  'ia64')  : ('ia64',      ('bin', 'ia64')),
+    ('arm',   'arm')   : ('arm',       'vcvarsarm.bat',        ('bin', 'arm')),
+    ('ia64',  'ia64')  : ('ia64',      'vcvars64.bat',         ('bin', 'ia64')),
 
 }
 
@@ -454,6 +464,53 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
 
 # debug("_LE2015_HOST_TARGET_CFG: %s", _LE2015_HOST_TARGET_CFG)
 
+# 9.0 (VS2008) to 8.0 (VS2005)
+
+_LE2008_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS = {
+
+    ('amd64', 'amd64') : ('amd64',     'vcvarsamd64.bat',      ('bin', 'amd64')),
+    ('amd64', 'x86') :   ('x86',       'vcvars32.bat',         ('bin', )),
+
+    ('x86',   'amd64') : ('x86_amd64', 'vcvarsx86_amd64.bat',  ('bin', 'x86_amd64')),
+    ('x86',   'x86')   : ('x86',       'vcvars32.bat',         ('bin', )),
+    ('x86',   'ia64')  : ('x86_ia64',  'vcvarsx86_ia64.bat',   ('bin', 'x86_ia64')),
+
+    ('arm64', 'amd64') : ('amd64',     'vcvarsamd64.bat',      ('bin', 'amd64')),
+    ('arm64', 'x86') :   ('x86',       'vcvars32.bat',         ('bin', )),
+
+    ('ia64',  'ia64')  : ('ia64',      'vcvarsia64.bat',       ('bin', 'ia64')),
+
+}
+
+_LE2008_HOST_TARGET_CFG = _host_target_config_factory(
+
+    label = 'LE2008',
+
+    host_all_hosts = OrderedDict([
+        ('amd64', ['amd64', 'x86']),
+        ('x86',   ['x86']),
+        ('arm64', ['amd64', 'x86']),
+        ('ia64',  ['ia64']),
+    ]),
+
+    host_all_targets = {
+        'amd64': ['amd64', 'x86'],
+        'x86':   ['x86', 'amd64', 'ia64'],
+        'arm64': ['amd64', 'x86'],
+        'ia64':  ['ia64'],
+    },
+
+    host_def_targets = {
+        'amd64': ['amd64', 'x86'],
+        'x86':   ['x86'],
+        'arm64': ['amd64', 'x86'],
+        'ia64':  ['ia64'],
+    },
+
+)
+
+# debug("_LE2008_HOST_TARGET_CFG: %s", _LE2008_HOST_TARGET_CFG)
+
 # 7.1 (VS2003) and earlier
 
 # For 7.1 (VS2003) and earlier, there are only x86 targets and the batch files
@@ -487,6 +544,8 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
 
 _CL_EXE_NAME = 'cl.exe'
 
+_VSWHERE_EXE = 'vswhere.exe'
+
 def get_msvc_version_numeric(msvc_version):
     """Get the raw version numbers from a MSVC_VERSION string, so it
     could be cast to float or other numeric values. For example, '14.0Exp'
@@ -566,9 +625,12 @@ def get_host_target(env, msvc_version, all_host_targets: bool=False):
     elif 143 > vernum_int >= 141:
         # 14.2 (VS2019) to 14.1 (VS2017)
         host_target_cfg = _LE2019_HOST_TARGET_CFG
-    elif 141 > vernum_int >= 80:
-        # 14.0 (VS2015) to 8.0 (VS2005)
+    elif 141 > vernum_int >= 100:
+        # 14.0 (VS2015) to 10.0 (VS2010)
         host_target_cfg = _LE2015_HOST_TARGET_CFG
+    elif 100 > vernum_int >= 80:
+        # 9.0 (VS2008) to 8.0 (VS2005)
+        host_target_cfg = _LE2008_HOST_TARGET_CFG
     else: # 80 > vernum_int
         # 7.1 (VS2003) and earlier
         host_target_cfg = _LE2003_HOST_TARGET_CFG
@@ -687,49 +749,72 @@ def _skip_sendtelemetry(env):
     "7.0",
     "6.0"]
 
-# if using vswhere, configure command line arguments to probe for installed VC editions
-_VCVER_TO_VSWHERE_VER = {
-    '14.3': [
-        ["-version", "[17.0, 18.0)"],  # default: Enterprise, Professional, Community  (order unpredictable?)
-        ["-version", "[17.0, 18.0)", "-products", "Microsoft.VisualStudio.Product.BuildTools"],  # BuildTools
-    ],
-    '14.2': [
-        ["-version", "[16.0, 17.0)"],  # default: Enterprise, Professional, Community  (order unpredictable?)
-        ["-version", "[16.0, 17.0)", "-products", "Microsoft.VisualStudio.Product.BuildTools"],  # BuildTools
-    ],
-    '14.1': [
-        ["-version", "[15.0, 16.0)"],  # default: Enterprise, Professional, Community (order unpredictable?)
-        ["-version", "[15.0, 16.0)", "-products", "Microsoft.VisualStudio.Product.BuildTools"],  # BuildTools
-    ],
-    '14.1Exp': [
-        ["-version", "[15.0, 16.0)", "-products", "Microsoft.VisualStudio.Product.WDExpress"],  # Express
-    ],
-}
-
+# VS2017 and later: use a single vswhere json query to find all installations
+
+# vswhere query:
+#    map vs major version to vc version (no suffix)
+#    build set of supported vc versions (including suffix)
+
+_VSWHERE_VSMAJOR_TO_VCVERSION = {}
+_VSWHERE_SUPPORTED_VCVER = set()
+
+for vs_major, vc_version, vc_ver_list in (
+    ('17', '14.3', None),
+    ('16', '14.2', None),
+    ('15', '14.1', ['14.1Exp']),
+):
+    _VSWHERE_VSMAJOR_TO_VCVERSION[vs_major] = vc_version
+    _VSWHERE_SUPPORTED_VCVER.add(vc_version)
+    if vc_ver_list:
+        for vc_ver in vc_ver_list:
+            _VSWHERE_SUPPORTED_VCVER.add(vc_ver)
+
+# vwhere query:
+#    build of set of candidate component ids
+#    preferred ranking: Enterprise, Professional, Community, BuildTools, Express
+#      Ent, Pro, Com, BT, Exp are in the same list
+#      Exp also has it's own list
+#    currently, only the express (Exp) suffix is expected
+
+_VSWHERE_COMPONENTID_CANDIDATES = set()
+_VSWHERE_COMPONENTID_RANKING = {}
+_VSWHERE_COMPONENTID_SUFFIX = {}
+_VSWHERE_COMPONENTID_SCONS_SUFFIX = {}
+
+for component_id, component_rank, component_suffix, scons_suffix in (
+    ('Enterprise',   140, 'Ent', ''),
+    ('Professional', 130, 'Pro', ''),
+    ('Community',    120, 'Com', ''),
+    ('BuildTools',   110, 'BT',  ''),
+    ('WDExpress',    100, 'Exp', 'Exp'),
+):
+    _VSWHERE_COMPONENTID_CANDIDATES.add(component_id)
+    _VSWHERE_COMPONENTID_RANKING[component_id] = component_rank
+    _VSWHERE_COMPONENTID_SUFFIX[component_id] = component_suffix
+    _VSWHERE_COMPONENTID_SCONS_SUFFIX[component_id] = scons_suffix
+
+# VS2015 and earlier: configure registry queries to probe for installed VC editions
 _VCVER_TO_PRODUCT_DIR = {
-    '14.3': [
-        (SCons.Util.HKEY_LOCAL_MACHINE, r'')],  # not set by this version
-    '14.2': [
-        (SCons.Util.HKEY_LOCAL_MACHINE, r'')],  # not set by this version
-    '14.1': [
-        (SCons.Util.HKEY_LOCAL_MACHINE, r'')],  # not set by this version
-    '14.1Exp': [
-        (SCons.Util.HKEY_LOCAL_MACHINE, r'')],  # not set by this version
     '14.0': [
-        (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\14.0\Setup\VC\ProductDir')],
+        (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\14.0\Setup\VC\ProductDir')
+    ],
     '14.0Exp': [
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\WDExpress\14.0\Setup\VS\ProductDir'),
-        (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\14.0\Setup\VC\ProductDir')],
+        (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\14.0\Setup\VC\ProductDir'),
+        (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\14.0\Setup\VC\ProductDir')
+    ],
     '12.0': [
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\12.0\Setup\VC\ProductDir'),
     ],
     '12.0Exp': [
+        (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\WDExpress\12.0\Setup\VS\ProductDir'),
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\12.0\Setup\VC\ProductDir'),
     ],
     '11.0': [
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\11.0\Setup\VC\ProductDir'),
     ],
     '11.0Exp': [
+        (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\WDExpress\11.0\Setup\VS\ProductDir'),
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\11.0\Setup\VC\ProductDir'),
     ],
     '10.0': [
@@ -739,17 +824,20 @@ def _skip_sendtelemetry(env):
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\10.0\Setup\VC\ProductDir'),
     ],
     '9.0': [
-        (SCons.Util.HKEY_CURRENT_USER, r'Microsoft\DevDiv\VCForPython\9.0\installdir',),
+        (SCons.Util.HKEY_CURRENT_USER, r'Microsoft\DevDiv\VCForPython\9.0\installdir',),  # vs root
+        (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\DevDiv\VCForPython\9.0\installdir',),  # vs root
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\9.0\Setup\VC\ProductDir',),
     ] if _VC90_Prefer_VCForPython else [
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\9.0\Setup\VC\ProductDir',),
-        (SCons.Util.HKEY_CURRENT_USER, r'Microsoft\DevDiv\VCForPython\9.0\installdir',),
+        (SCons.Util.HKEY_CURRENT_USER, r'Microsoft\DevDiv\VCForPython\9.0\installdir',),  # vs root
+        (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\DevDiv\VCForPython\9.0\installdir',),  # vs root
     ],
     '9.0Exp': [
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\9.0\Setup\VC\ProductDir'),
     ],
     '8.0': [
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\8.0\Setup\VC\ProductDir'),
+        (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\8.0\Setup\VC\ProductDir'),
     ],
     '8.0Exp': [
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\8.0\Setup\VC\ProductDir'),
@@ -765,6 +853,62 @@ def _skip_sendtelemetry(env):
     ]
 }
 
+# detect ide binaries
+
+VS2022_VS2002_DEV = (
+    MSVC.Kind.IDE_PROGRAM_DEVENV_COM,  # devenv.com
+)
+
+VS1998_DEV = (
+    MSVC.Kind.IDE_PROGRAM_MSDEV_COM,  # MSDEV.COM
+)
+
+VS2017_EXP = (
+    MSVC.Kind.IDE_PROGRAM_WDEXPRESS_EXE,  # WDExpress.exe
+)
+
+VS2015_VS2012_EXP = (
+    MSVC.Kind.IDE_PROGRAM_WDEXPRESS_EXE,     # WDExpress.exe [Desktop]
+    MSVC.Kind.IDE_PROGRAM_VSWINEXPRESS_EXE,  # VSWinExpress.exe [Windows]
+    MSVC.Kind.IDE_PROGRAM_VWDEXPRESS_EXE,    # VWDExpress.exe [Web]
+)
+
+VS2010_VS2005_EXP = (
+    MSVC.Kind.IDE_PROGRAM_VCEXPRESS_EXE,
+)
+
+# detect productdir kind
+
+_DETECT = MSVC.Kind.VCVER_KIND_DETECT
+
+_VCVER_KIND_DETECT = {
+
+    #   'VCVer':   (relpath from pdir to vsroot, path from vsroot to ide binaries, ide binaries)
+
+    '14.3':    _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV),  # 2022
+    '14.2':    _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV),  # 2019
+    '14.1':    _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2017_EXP),  # 2017
+    '14.1Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2017_EXP),  # 2017
+
+    '14.0':    _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP),  # 2015
+    '14.0Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP),  # 2015
+    '12.0':    _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP),  # 2013
+    '12.0Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP),  # 2013
+    '11.0':    _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP),  # 2012
+    '11.0Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP),  # 2012
+
+    '10.0':    _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP),  # 2010
+    '10.0Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP),  # 2010
+    '9.0':     _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP),  # 2008
+    '9.0Exp':  _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP),  # 2008
+    '8.0':     _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP),  # 2005
+    '8.0Exp':  _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP),  # 2005
+
+    '7.1':     _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV),  # 2003
+    '7.0':     _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV),  # 2001
+
+    '6.0':     _DETECT(root='..', path=r'Common\MSDev98\Bin', programs=VS1998_DEV),  # 1998
+}
 
 def msvc_version_to_maj_min(msvc_version):
     msvc_version_numeric = get_msvc_version_numeric(msvc_version)
@@ -780,33 +924,581 @@ def msvc_version_to_maj_min(msvc_version):
         raise ValueError("Unrecognized version %s (%s)" % (msvc_version,msvc_version_numeric)) from None
 
 
-VSWHERE_PATHS = [os.path.join(p,'vswhere.exe') for p in [
+_VSWHERE_EXEGROUP_MSVS = [os.path.join(p, _VSWHERE_EXE) for p in [
+    # For bug 3333: support default location of vswhere for both
+    # 64 and 32 bit windows installs.
+    # For bug 3542: also accommodate not being on C: drive.
     os.path.expandvars(r"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer"),
     os.path.expandvars(r"%ProgramFiles%\Microsoft Visual Studio\Installer"),
+]]
+
+_VSWHERE_EXEGROUP_PKGMGR = [os.path.join(p, _VSWHERE_EXE) for p in [
     os.path.expandvars(r"%ChocolateyInstall%\bin"),
+    os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\WinGet\Links"),
+    os.path.expanduser(r"~\scoop\shims"),
+    os.path.expandvars(r"%SCOOP%\shims"),
 ]]
 
+_VSWhereBinary = namedtuple('_VSWhereBinary', [
+    'vswhere_exe',
+    'vswhere_norm',
+])
+
+class VSWhereBinary(_VSWhereBinary, MSVC.Util.AutoInitialize):
+
+    _UNDEFINED_VSWHERE_BINARY = None
+
+    _cache_vswhere_paths = {}
+
+    @classmethod
+    def _initialize(cls):
+        vswhere_binary = cls(
+            vswhere_exe=None,
+            vswhere_norm=None,
+        )
+        cls._UNDEFINED_VSWHERE_BINARY = vswhere_binary
+        for symbol in (None, ''):
+            cls._cache_vswhere_paths[symbol] = vswhere_binary
+
+    @classmethod
+    def factory(cls, vswhere_exe):
+
+        if not vswhere_exe:
+            return cls._UNDEFINED_VSWHERE_BINARY
+
+        vswhere_binary = cls._cache_vswhere_paths.get(vswhere_exe)
+        if vswhere_binary:
+            return vswhere_binary
+
+        vswhere_norm = MSVC.Util.normalize_path(vswhere_exe)
+
+        vswhere_binary = cls._cache_vswhere_paths.get(vswhere_norm)
+        if vswhere_binary:
+            return vswhere_binary
+
+        vswhere_binary = cls(
+            vswhere_exe=vswhere_exe,
+            vswhere_norm=vswhere_norm,
+        )
+
+        cls._cache_vswhere_paths[vswhere_exe] = vswhere_binary
+        cls._cache_vswhere_paths[vswhere_norm] = vswhere_binary
+
+        return vswhere_binary
+
+class _VSWhereExecutable(MSVC.Util.AutoInitialize):
+
+    debug_extra = None
+
+    class _VSWhereUserPriority(enum.IntEnum):
+        HIGH = 0               # before msvs locations
+        DEFAULT = enum.auto()  # after msvs locations and before pkgmgr locations
+        LOW = enum.auto()      # after pkgmgr locations
+
+    priority_default = _VSWhereUserPriority.DEFAULT
+    priority_map = {}
+    priority_symbols = []
+    priority_indmap = {}
+
+    for e in _VSWhereUserPriority:
+        for symbol in (e.name.lower(), e.name.lower().capitalize(), e.name):
+            priority_map[symbol] = e.value
+            priority_symbols.append(symbol)
+        priority_indmap[e.value] = e.name
+
+    UNDEFINED_VSWHERE_BINARY = VSWhereBinary.factory(None)
+
+    @classmethod
+    def reset(cls):
+
+        cls._vswhere_exegroups = None
+        cls._vswhere_exegroups_user = None
+        cls._vswhere_exegroup_msvs = None
+        cls._vswhere_exegroup_pkgmgr = None
+
+        cls.vswhere_frozen_flag = False
+        cls.vswhere_frozen_binary = None
+
+    @classmethod
+    def _initialize(cls):
+        cls.debug_extra = common.debug_extra(cls)
+        cls.reset()
+
+    @classmethod
+    def _vswhere_exegroup_binaries(cls, vswhere_exe_list, label):
+        vswhere_binaries = []
+        for vswhere_exe in vswhere_exe_list:
+            if not os.path.exists(vswhere_exe):
+                continue
+            vswhere_binary = VSWhereBinary.factory(vswhere_exe)
+            vswhere_binaries.append(vswhere_binary)
+            debug(
+                "insert exegroup=%s, vswhere_binary=%s",
+                label, vswhere_binary, extra=cls.debug_extra
+            )
+        return vswhere_binaries
+
+    @classmethod
+    def _get_vswhere_exegroups(cls):
+
+        if cls._vswhere_exegroups is None:
+
+            cls._vswhere_exegroup_msvs = cls._vswhere_exegroup_binaries(_VSWHERE_EXEGROUP_MSVS, 'MSVS')
+            cls._vswhere_exegroup_pkgmgr = cls._vswhere_exegroup_binaries(_VSWHERE_EXEGROUP_PKGMGR, 'PKGMGR')
+
+            cls._vswhere_exegroups_user = [
+                [] for e in cls._VSWhereUserPriority
+            ]
+
+            vswhere_exegroups = [
+                cls._vswhere_exegroups_user[cls._VSWhereUserPriority.HIGH],
+                cls._vswhere_exegroup_msvs,
+                cls._vswhere_exegroups_user[cls._VSWhereUserPriority.DEFAULT],
+                cls._vswhere_exegroup_pkgmgr,
+                cls._vswhere_exegroups_user[cls._VSWhereUserPriority.LOW],
+            ]
+
+            cls._vswhere_exegroups = vswhere_exegroups
+
+        return cls._vswhere_exegroups
+
+    @classmethod
+    def _get_vswhere_exegroups_user(cls):
+
+        if cls._vswhere_exegroups_user is None:
+            cls._get_vswhere_exegroups()
+
+        return cls._vswhere_exegroups_user
+
+    @classmethod
+    def _vswhere_exegroup_binary(cls, exegroup):
+        # first vswhere binary in group or UNDEFINED_VSWHERE_BINARY
+        vswhere_binary = exegroup[0] if exegroup else cls.UNDEFINED_VSWHERE_BINARY
+        return vswhere_binary
+
+    @classmethod
+    def _vswhere_current_binary(cls):
+        # first vswhere binary in priority order or UNDEFINED_VSWHERE_BINARY
+        vswhere_binary = cls.UNDEFINED_VSWHERE_BINARY
+        vswhere_exegroups = cls._get_vswhere_exegroups()
+        for exegroup in vswhere_exegroups:
+            binary = cls._vswhere_exegroup_binary(exegroup)
+            if binary == cls.UNDEFINED_VSWHERE_BINARY:
+                continue
+            vswhere_binary = binary
+            break
+        return vswhere_binary
+
+    @classmethod
+    def _vswhere_all_executables(cls):
+        # unique vswhere executables in priority order
+        vswhere_exe_list = []
+        vswhere_norm_set = set()
+        vswhere_exegroups = cls._get_vswhere_exegroups()
+        for exegroup in vswhere_exegroups:
+            for vswhere_binary in exegroup:
+                if vswhere_binary.vswhere_norm in vswhere_norm_set:
+                    continue
+                vswhere_norm_set.add(vswhere_binary.vswhere_norm)
+                vswhere_exe_list.append(vswhere_binary.vswhere_exe)
+        return vswhere_exe_list
+
+    @classmethod
+    def is_frozen(cls) -> bool:
+        rval = bool(cls.vswhere_frozen_flag)
+        return rval
+
+    @classmethod
+    def freeze_vswhere_binary(cls):
+        if not cls.vswhere_frozen_flag:
+            cls.vswhere_frozen_flag = True
+            cls.vswhere_frozen_binary = cls._vswhere_current_binary()
+            debug("freeze=%s", cls.vswhere_frozen_binary, extra=cls.debug_extra)
+        return cls.vswhere_frozen_binary
+
+    @classmethod
+    def freeze_vswhere_executable(cls):
+        vswhere_binary = cls.freeze_vswhere_binary()
+        vswhere_exe = vswhere_binary.vswhere_exe
+        return vswhere_exe
+
+    @classmethod
+    def get_vswhere_executable(cls):
+        if cls.vswhere_frozen_flag:
+            vswhere_binary = cls.vswhere_frozen_binary
+        else:
+            vswhere_binary = cls._vswhere_current_binary()
+        vswhere_exe = vswhere_binary.vswhere_exe
+        debug("vswhere_exe=%s", repr(vswhere_exe), extra=cls.debug_extra)
+        return vswhere_exe
+
+    @classmethod
+    def _vswhere_priority_group(cls, priority):
+        if not priority:
+            group = cls.priority_default
+        else:
+            group = cls.priority_map.get(priority)
+            if group is None:
+                msg = f'Value specified for vswhere executable priority is not supported: {priority!r}:\n' \
+                      f'  Valid values are: {cls.priority_symbols}'
+                debug(f'VSWhereUserError: {msg}', extra=cls.debug_extra)
+                raise VSWhereUserError(msg)
+        return group
+
+    @classmethod
+    def register_vswhere_executable(cls, vswhere_exe, priority=None):
+
+        vswhere_binary = cls.UNDEFINED_VSWHERE_BINARY
+
+        if not vswhere_exe:
+            # ignore: None or empty
+            return vswhere_binary
+
+        if not os.path.exists(vswhere_exe):
+            msg = f'Specified vswhere executable not found: {vswhere_exe!r}.'
+            debug(f'VSWhereUserError: {msg}', extra=cls.debug_extra)
+            raise VSWhereUserError(msg)
+
+        group = cls._vswhere_priority_group(priority)
+
+        vswhere_binary = VSWhereBinary.factory(vswhere_exe)
+
+        if cls.vswhere_frozen_flag:
+
+            if vswhere_binary.vswhere_norm == cls.vswhere_frozen_binary.vswhere_norm:
+                # ignore: user executable == frozen executable
+                return vswhere_binary
+
+            msg = 'A different vswhere execuable cannot be requested after initial detetection:\n' \
+                  f'  initial vswhere executable:  {cls.vswhere_frozen_binary.vswhere_exe!r}\n' \
+                  f'  request vswhere executable: {vswhere_binary.vswhere_exe!r}'
+
+            debug(f'VSWhereUserError: {msg}', extra=cls.debug_extra)
+            raise VSWhereUserError(msg)
+
+        vswhere_exegroups_user = cls._get_vswhere_exegroups_user()
+
+        exegroup = vswhere_exegroups_user[group]
+        group_binary = cls._vswhere_exegroup_binary(exegroup)
+
+        if vswhere_binary.vswhere_norm == group_binary.vswhere_norm:
+            # ignore: user executable == exegroup[0] executable
+            return vswhere_binary
+
+        exegroup.insert(0, vswhere_binary)
+        debug(
+            "insert exegroup=user[%s], vswhere_binary=%s",
+            cls.priority_indmap[group], vswhere_binary, extra=cls.debug_extra
+        )
+
+        return vswhere_binary
+
+    @classmethod
+    def vswhere_freeze_executable(cls, vswhere_exe):
+        vswhere_binary = cls.register_vswhere_executable(vswhere_exe, priority='high')
+        frozen_binary = cls.freeze_vswhere_binary()
+        return frozen_binary, vswhere_binary
+
+    @classmethod
+    def vswhere_freeze_env(cls, env):
+
+        if env is None:
+            # no environment, VSWHERE undefined
+            vswhere_exe = None
+            write_vswhere = False
+        elif not env.get('VSWHERE'):
+            # environment, VSWHERE undefined/none/empty
+            vswhere_exe = None
+            write_vswhere = True
+        else:
+            # environment, VSWHERE defined
+            vswhere_exe = env.subst('$VSWHERE')
+            write_vswhere = False
+
+        frozen_binary, vswhere_binary = cls.vswhere_freeze_executable(vswhere_exe)
+
+        if write_vswhere and frozen_binary.vswhere_norm != vswhere_binary.vswhere_norm:
+            env['VSWHERE'] = frozen_binary.vswhere_exe
+            debug(
+                "env['VSWHERE']=%s",
+                repr(frozen_binary.vswhere_exe), extra=cls.debug_extra
+            )
+
+        return frozen_binary, vswhere_binary
+
+# external use
+
+def vswhere_register_executable(vswhere_exe, priority=None, freeze=False):
+    debug(
+        'register vswhere_exe=%s, priority=%s, freeze=%s',
+        repr(vswhere_exe), repr(priority), repr(freeze)
+    )
+    _VSWhereExecutable.register_vswhere_executable(vswhere_exe, priority=priority)
+    if freeze:
+        _VSWhereExecutable.freeze_vswhere_executable()
+    rval = _VSWhereExecutable.get_vswhere_executable()
+    debug('current vswhere_exe=%s, is_frozen=%s', repr(rval), _VSWhereExecutable.is_frozen())
+    return vswhere_exe
+
+def vswhere_get_executable():
+    debug('')
+    vswhere_exe = _VSWhereExecutable.get_vswhere_executable()
+    return vswhere_exe
+
+def vswhere_freeze_executable():
+    debug('')
+    vswhere_exe = _VSWhereExecutable.freeze_vswhere_executable()
+    return vswhere_exe
+
+# internal use only
+vswhere_freeze_env = _VSWhereExecutable.vswhere_freeze_env
+
 def msvc_find_vswhere():
     """ Find the location of vswhere """
-    # For bug 3333: support default location of vswhere for both
-    # 64 and 32 bit windows installs.
-    # For bug 3542: also accommodate not being on C: drive.
     # NB: this gets called from testsuite on non-Windows platforms.
     # Whether that makes sense or not, don't break it for those.
-    vswhere_path = None
-    for pf in VSWHERE_PATHS:
-        if os.path.exists(pf):
-            vswhere_path = pf
+    vswhere_path = _VSWhereExecutable.get_vswhere_executable()
+    return vswhere_path
+
+_MSVCInstance = namedtuple('_MSVCInstance', [
+    'vc_path',
+    'vc_version',
+    'vc_version_numeric',
+    'vc_version_scons',
+    'vc_release',
+    'vc_component_id',
+    'vc_component_rank',
+    'vc_component_suffix',
+])
+
+class MSVCInstance(_MSVCInstance):
+
+    @staticmethod
+    def msvc_instances_default_order(a, b):
+        # vc version numeric: descending order
+        if a.vc_version_numeric != b.vc_version_numeric:
+            return 1 if a.vc_version_numeric < b.vc_version_numeric else -1
+        # vc release: descending order (release, preview)
+        if a.vc_release != b.vc_release:
+            return 1 if a.vc_release < b.vc_release else -1
+        # component rank: descending order
+        if a.vc_component_rank != b.vc_component_rank:
+            return 1 if a.vc_component_rank < b.vc_component_rank else -1
+        return 0
+
+class _VSWhere(MSVC.Util.AutoInitialize):
+
+    debug_extra = None
+
+    @classmethod
+    def reset(cls):
+
+        cls.seen_vswhere = set()
+        cls.seen_root = set()
+
+        cls.msvc_instances = []
+        cls.msvc_map = {}
+
+    @classmethod
+    def _initialize(cls):
+        cls.debug_extra = common.debug_extra(cls)
+        cls.reset()
+
+    @classmethod
+    def _filter_vswhere_binary(cls, vswhere_binary):
+
+        vswhere_norm = None
+
+        if vswhere_binary.vswhere_norm not in cls.seen_vswhere:
+            cls.seen_vswhere.add(vswhere_binary.vswhere_norm)
+            vswhere_norm = vswhere_binary.vswhere_norm
+
+        return vswhere_norm
+
+    @classmethod
+    def _new_roots_discovered(cls):
+        if len(cls.seen_vswhere) > 1:
+            raise MSVCInternalError(f'vswhere discovered new msvc installations after initial detection')
+
+    @classmethod
+    def _vswhere_query_json_output(cls, vswhere_exe, vswhere_args):
+
+        vswhere_json = None
+
+        once = True
+        while once:
+            once = False
+            # using break for single exit (unless exception)
+
+            vswhere_cmd = [vswhere_exe] + vswhere_args + ['-format', 'json', '-utf8']
+            debug("running: %s", vswhere_cmd, extra=cls.debug_extra)
+
+            try:
+                cp = subprocess.run(vswhere_cmd, stdout=PIPE, stderr=PIPE, check=True)
+            except OSError as e:
+                errmsg = str(e)
+                debug("%s: %s", type(e).__name__, errmsg, extra=cls.debug_extra)
+                break
+            except Exception as e:
+                errmsg = str(e)
+                debug("%s: %s", type(e).__name__, errmsg, extra=cls.debug_extra)
+                raise
+
+            if not cp.stdout:
+                debug("no vswhere information returned", extra=cls.debug_extra)
+                break
+
+            vswhere_output = cp.stdout.decode('utf8', errors='replace')
+            if not vswhere_output:
+                debug("no vswhere information output", extra=cls.debug_extra)
+                break
+
+            try:
+                vswhere_output_json = json.loads(vswhere_output)
+            except json.decoder.JSONDecodeError:
+                debug("json decode exception loading vswhere output", extra=cls.debug_extra)
+                break
+
+            vswhere_json = vswhere_output_json
             break
 
-    return vswhere_path
+        debug(
+            'vswhere_json=%s, vswhere_exe=%s',
+            bool(vswhere_json), repr(vswhere_exe), extra=cls.debug_extra
+        )
+
+        return vswhere_json
+
+    @classmethod
+    def vswhere_update_msvc_map(cls, vswhere_exe):
+
+        frozen_binary, vswhere_binary = _VSWhereExecutable.vswhere_freeze_executable(vswhere_exe)
+
+        vswhere_norm = cls._filter_vswhere_binary(frozen_binary)
+        if not vswhere_norm:
+            return cls.msvc_map
+
+        debug('vswhere_norm=%s', repr(vswhere_norm), extra=cls.debug_extra)
+
+        vswhere_json = cls._vswhere_query_json_output(
+            vswhere_norm,
+            ['-all', '-products', '*']
+        )
+
+        if not vswhere_json:
+            return cls.msvc_map
+
+        n_instances = len(cls.msvc_instances)
+
+        for instance in vswhere_json:
+
+            #print(json.dumps(instance, indent=4, sort_keys=True))
+
+            installation_path = instance.get('installationPath')
+            if not installation_path or not os.path.exists(installation_path):
+                continue
+
+            vc_path = os.path.join(installation_path, 'VC')
+            if not os.path.exists(vc_path):
+                continue
+
+            vc_root = MSVC.Util.normalize_path(vc_path)
+            if vc_root in cls.seen_root:
+                continue
+            cls.seen_root.add(vc_root)
+
+            installation_version = instance.get('installationVersion')
+            if not installation_version:
+                continue
+
+            vs_major = installation_version.split('.')[0]
+            if not vs_major in _VSWHERE_VSMAJOR_TO_VCVERSION:
+                debug('ignore vs_major: %s', vs_major, extra=cls.debug_extra)
+                continue
+
+            vc_version = _VSWHERE_VSMAJOR_TO_VCVERSION[vs_major]
+
+            product_id = instance.get('productId')
+            if not product_id:
+                continue
+
+            component_id = product_id.split('.')[-1]
+            if component_id not in _VSWHERE_COMPONENTID_CANDIDATES:
+                debug('ignore component_id: %s', component_id, extra=cls.debug_extra)
+                continue
+
+            component_rank = _VSWHERE_COMPONENTID_RANKING.get(component_id,0)
+            if component_rank == 0:
+                raise MSVCInternalError(f'unknown component_rank for component_id: {component_id!r}')
+
+            scons_suffix = _VSWHERE_COMPONENTID_SCONS_SUFFIX[component_id]
+
+            if scons_suffix:
+                vc_version_scons = vc_version + scons_suffix
+            else:
+                vc_version_scons = vc_version
+
+            is_prerelease = True if instance.get('isPrerelease', False) else False
+            is_release = False if is_prerelease else True
+
+            msvc_instance = MSVCInstance(
+                vc_path = vc_path,
+                vc_version = vc_version,
+                vc_version_numeric = float(vc_version),
+                vc_version_scons = vc_version_scons,
+                vc_release = is_release,
+                vc_component_id = component_id,
+                vc_component_rank = component_rank,
+                vc_component_suffix = component_suffix,
+            )
+
+            cls.msvc_instances.append(msvc_instance)
+
+        new_roots = bool(len(cls.msvc_instances) > n_instances)
+        if new_roots:
+
+            cls.msvc_instances = sorted(
+                cls.msvc_instances,
+                key=cmp_to_key(MSVCInstance.msvc_instances_default_order)
+            )
+
+            cls.msvc_map = {}
+
+            for msvc_instance in cls.msvc_instances:
+
+                debug(
+                    'msvc instance: msvc_version=%s, is_release=%s, component_id=%s, vc_path=%s',
+                    repr(msvc_instance.vc_version_scons), msvc_instance.vc_release,
+                    repr(msvc_instance.vc_component_id), repr(msvc_instance.vc_path),
+                    extra=cls.debug_extra
+                )
 
-def find_vc_pdir_vswhere(msvc_version, env=None):
+                key = (msvc_instance.vc_version_scons, msvc_instance.vc_release)
+                cls.msvc_map.setdefault(key,[]).append(msvc_instance)
+
+                if msvc_instance.vc_version_scons == msvc_instance.vc_version:
+                    continue
+
+                key = (msvc_instance.vc_version, msvc_instance.vc_release)
+                cls.msvc_map.setdefault(key,[]).append(msvc_instance)
+
+            cls._new_roots_discovered()
+
+        debug(
+            'new_roots=%s, msvc_instances=%s',
+            new_roots, len(cls.msvc_instances), extra=cls.debug_extra
+        )
+
+        return cls.msvc_map
+
+_cache_pdir_vswhere_queries = {}
+
+def _find_vc_pdir_vswhere(msvc_version, vswhere_exe):
     """ Find the MSVC product directory using the vswhere program.
 
     Args:
         msvc_version: MSVC version to search for
-        env: optional to look up VSWHERE variable
+        vswhere_exe: vswhere executable or None
 
     Returns:
         MSVC install dir or None
@@ -815,57 +1507,178 @@ def find_vc_pdir_vswhere(msvc_version, env=None):
         UnsupportedVersion: if the version is not known by this file
 
     """
+    global _cache_pdir_vswhere_queries
+
+    msvc_map = _VSWhere.vswhere_update_msvc_map(vswhere_exe)
+    if not msvc_map:
+        return None
+
+    rval = _cache_pdir_vswhere_queries.get(msvc_version, UNDEFINED)
+    if rval != UNDEFINED:
+        debug('msvc_version=%s, pdir=%s', repr(msvc_version), repr(rval))
+        return rval
+
+    if msvc_version not in _VSWHERE_SUPPORTED_VCVER:
+        debug("Unknown version of MSVC: %s", msvc_version)
+        raise UnsupportedVersion("Unknown version %s" % msvc_version)
+
+    is_release = True
+    key = (msvc_version, is_release)
+
+    msvc_instances = msvc_map.get(key, UNDEFINED)
+    if msvc_instances == UNDEFINED:
+        debug(
+            'msvc instances lookup failed: msvc_version=%s, is_release=%s',
+            repr(msvc_version), repr(is_release)
+        )
+        msvc_instances = []
+
+    save_pdir_kind = []
+
+    pdir = None
+    kind_t = None
+
+    for msvc_instance in msvc_instances:
+
+        vcdir = msvc_instance.vc_path
+
+        vckind_t = MSVC.Kind.msvc_version_pdir_vswhere_kind(msvc_version, vcdir, _VCVER_KIND_DETECT[msvc_version])
+        if vckind_t.skip:
+            if vckind_t.save:
+                debug('save kind: msvc_version=%s, pdir=%s', repr(msvc_version), repr(vcdir))
+                save_pdir_kind.append((vcdir, vckind_t))
+            else:
+                debug('skip kind: msvc_version=%s, pdir=%s', repr(msvc_version), repr(vcdir))
+            continue
+
+        pdir = vcdir
+        kind_t = vckind_t
+        break
+
+    if not pdir and not kind_t:
+        if save_pdir_kind:
+            pdir, kind_t = save_pdir_kind[0]
+
+    MSVC.Kind.msvc_version_register_kind(msvc_version, kind_t)
+
+    debug('msvc_version=%s, pdir=%s', repr(msvc_version), repr(pdir))
+    _cache_pdir_vswhere_queries[msvc_version] = pdir
+
+    return pdir
+
+_cache_pdir_registry_queries = {}
+
+def _find_vc_pdir_registry(msvc_version):
+    """ Find the MSVC product directory using the registry.
+
+    Args:
+        msvc_version: MSVC version to search for
+
+    Returns:
+        MSVC install dir or None
+
+    Raises:
+        UnsupportedVersion: if the version is not known by this file
+
+    """
+    global _cache_pdir_registry_queries
+
+    rval = _cache_pdir_registry_queries.get(msvc_version, UNDEFINED)
+    if rval != UNDEFINED:
+        debug('msvc_version=%s, pdir=%s', repr(msvc_version), repr(rval))
+        return rval
+
     try:
-        vswhere_version = _VCVER_TO_VSWHERE_VER[msvc_version]
+        regkeys = _VCVER_TO_PRODUCT_DIR[msvc_version]
     except KeyError:
         debug("Unknown version of MSVC: %s", msvc_version)
         raise UnsupportedVersion("Unknown version %s" % msvc_version) from None
 
-    if env is None or not env.get('VSWHERE'):
-        vswhere_path = msvc_find_vswhere()
-    else:
-        vswhere_path = env.subst('$VSWHERE')
-
-    if vswhere_path is None:
-        return None
+    save_pdir_kind = []
 
-    debug('VSWHERE: %s', vswhere_path)
-    for vswhere_version_args in vswhere_version:
+    is_win64 = common.is_win64()
 
-        vswhere_cmd = [vswhere_path] + vswhere_version_args + ["-property", "installationPath"]
+    pdir = None
+    kind_t = None
 
-        debug("running: %s", vswhere_cmd)
+    root = 'Software\\'
+    for hkroot, key in regkeys:
 
-        # TODO: Python 3.7
-        #  cp = subprocess.run(vswhere_cmd, capture_output=True, check=True)  # 3.7+ only
-        cp = subprocess.run(vswhere_cmd, stdout=PIPE, stderr=PIPE, check=True)
+        if not hkroot or not key:
+            continue
 
-        if cp.stdout:
-            # vswhere could return multiple lines, e.g. if Build Tools
-            # and {Community,Professional,Enterprise} are both installed.
-            # We could define a way to pick the one we prefer, but since
-            # this data is currently only used to make a check for existence,
-            # returning the first hit should be good enough.
-            lines = cp.stdout.decode("mbcs").splitlines()
-            return os.path.join(lines[0], 'VC')
+        if is_win64:
+            msregkeys = [root + 'Wow6432Node\\' + key, root + key]
         else:
-            # We found vswhere, but no install info available for this version
-            pass
+            msregkeys = [root + key]
 
-    return None
+        vcdir = None
+        for msregkey in msregkeys:
+            debug('trying VC registry key %s', repr(msregkey))
+            try:
+                vcdir = common.read_reg(msregkey, hkroot)
+            except OSError:
+                continue
+            if vcdir:
+                break
 
+        if not vcdir:
+            debug('no VC registry key %s', repr(key))
+            continue
 
-def find_vc_pdir(env, msvc_version):
-    """Find the MSVC product directory for the given version.
+        is_vcforpython = False
+
+        is_vsroot = False
+        if msvc_version == '9.0' and key.lower().endswith('\\vcforpython\\9.0\\installdir'):
+            # Visual C++ for Python registry key is VS installdir (root) not VC productdir
+            is_vsroot = True
+            is_vcforpython = True
+        elif msvc_version in ('14.0Exp', '12.0Exp', '11.0Exp') and key.lower().endswith('\\setup\\vs\\productdir'):
+            # Visual Studio 2015/2013/2012 Express is VS productdir (root) not VC productdir
+            is_vsroot = True
+
+        if is_vsroot:
+            vcdir = os.path.join(vcdir, 'VC')
+            debug('convert vs root to vc dir: %s', repr(vcdir))
+
+        if not os.path.exists(vcdir):
+            debug('reg says dir is %s, but it does not exist. (ignoring)', repr(vcdir))
+            continue
+
+        vckind_t = MSVC.Kind.msvc_version_pdir_registry_kind(msvc_version, vcdir, _VCVER_KIND_DETECT[msvc_version], is_vcforpython)
+        if vckind_t.skip:
+            if vckind_t.save:
+                debug('save kind: msvc_version=%s, pdir=%s', repr(msvc_version), repr(vcdir))
+                save_pdir_kind.append((vcdir, vckind_t))
+            else:
+                debug('skip kind: msvc_version=%s, pdir=%s', repr(msvc_version), repr(vcdir))
+            continue
+
+        pdir = vcdir
+        kind_t = vckind_t
+        break
 
-    Tries to look up the path using a registry key from the table
-    _VCVER_TO_PRODUCT_DIR; if there is no key, calls find_vc_pdir_wshere
-    for help instead.
+    if not pdir and not kind_t:
+        if save_pdir_kind:
+            pdir, kind_t = save_pdir_kind[0]
+
+    MSVC.Kind.msvc_version_register_kind(msvc_version, kind_t)
+
+    debug('msvc_version=%s, pdir=%s', repr(msvc_version), repr(pdir))
+    _cache_pdir_registry_queries[msvc_version] = pdir
+
+    return pdir
+
+def _find_vc_pdir(msvc_version, vswhere_exe):
+    """Find the MSVC product directory for the given version.
 
     Args:
         msvc_version: str
             msvc version (major.minor, e.g. 10.0)
 
+        vswhere_exe: str
+            vswhere executable or None
+
     Returns:
         str: Path found in registry, or None
 
@@ -875,52 +1688,66 @@ def find_vc_pdir(env, msvc_version):
         UnsupportedVersion inherits from VisualCException.
 
     """
-    root = 'Software\\'
-    try:
-        hkeys = _VCVER_TO_PRODUCT_DIR[msvc_version]
-    except KeyError:
-        debug("Unknown version of MSVC: %s", msvc_version)
-        raise UnsupportedVersion("Unknown version %s" % msvc_version) from None
 
-    for hkroot, key in hkeys:
-        try:
-            comps = None
-            if not key:
-                comps = find_vc_pdir_vswhere(msvc_version, env)
-                if not comps:
-                    debug('no VC found for version %s', repr(msvc_version))
-                    raise OSError
-                debug('VC found: %s', repr(msvc_version))
-                return comps
-            else:
-                if common.is_win64():
-                    try:
-                        # ordinarily at win64, try Wow6432Node first.
-                        comps = common.read_reg(root + 'Wow6432Node\\' + key, hkroot)
-                    except OSError:
-                        # at Microsoft Visual Studio for Python 2.7, value is not in Wow6432Node
-                        pass
-                if not comps:
-                    # not Win64, or Microsoft Visual Studio for Python 2.7
-                    comps = common.read_reg(root + key, hkroot)
-        except OSError:
-            debug('no VC registry key %s', repr(key))
-        else:
-            if msvc_version == '9.0':
-                if key.lower().endswith('\\vcforpython\\9.0\\installdir'):
-                    # Visual C++ for Python registry key is installdir (root) not productdir (vc)
-                    comps = os.path.join(comps, 'VC')
-            elif msvc_version == '14.0Exp':
-                if key.lower().endswith('\\setup\\vs\\productdir'):
-                    # Visual Studio 14.0 Express registry key is installdir (root) not productdir (vc)
-                    comps = os.path.join(comps, 'VC')
-            debug('found VC in registry: %s', comps)
-            if os.path.exists(comps):
-                return comps
-            else:
-                debug('reg says dir is %s, but it does not exist. (ignoring)', comps)
+    if msvc_version in _VSWHERE_SUPPORTED_VCVER:
+
+        pdir = _find_vc_pdir_vswhere(msvc_version, vswhere_exe)
+        if pdir:
+            debug('VC found: %s, dir=%s', repr(msvc_version), repr(pdir))
+            return pdir
+
+    elif msvc_version in _VCVER_TO_PRODUCT_DIR:
+
+        pdir = _find_vc_pdir_registry(msvc_version)
+        if pdir:
+            debug('VC found: %s, dir=%s', repr(msvc_version), repr(pdir))
+            return pdir
+
+    else:
+
+        debug("Unknown version of MSVC: %s", repr(msvc_version))
+        raise UnsupportedVersion("Unknown version %s" % repr(msvc_version)) from None
+
+    debug('no VC found for version %s', repr(msvc_version))
     return None
 
+def find_vc_pdir(msvc_version, env=None):
+    """Find the MSVC product directory for the given version.
+
+    Args:
+        msvc_version: str
+            msvc version (major.minor, e.g. 10.0)
+        env:
+            optional to look up VSWHERE variable
+
+    Returns:
+        str: Path found in registry, or None
+
+    Raises:
+        UnsupportedVersion: if the version is not known by this file.
+
+        UnsupportedVersion inherits from VisualCException.
+
+    """
+
+    frozen_binary, _ = _VSWhereExecutable.vswhere_freeze_env(env)
+
+    pdir = _find_vc_pdir(msvc_version, frozen_binary.vswhere_exe)
+    debug('pdir=%s', repr(pdir))
+
+    return pdir
+
+# register find_vc_pdir function with Kind routines
+# in case of unknown msvc_version (just in case)
+MSVC.Kind.register_msvc_version_pdir_func(find_vc_pdir)
+
+def _reset_vc_pdir():
+    debug('reset pdir caches')
+    global _cache_pdir_vswhere_queries
+    global _cache_pdir_registry_queries
+    _cache_pdir_vswhere_queries = {}
+    _cache_pdir_registry_queries = {}
+
 def find_batch_file(msvc_version, host_arch, target_arch, pdir):
     """
     Find the location of the batch script which should set up the compiler
@@ -940,6 +1767,7 @@ def find_batch_file(msvc_version, host_arch, target_arch, pdir):
     arg = ''
     vcdir = None
     clexe = None
+    depbat = None
 
     if vernum_int >= 143:
         # 14.3 (VS2022) and later
@@ -953,15 +1781,26 @@ def find_batch_file(msvc_version, host_arch, target_arch, pdir):
         batfile, _ = _LE2019_HOST_TARGET_BATCHFILE_CLPATHCOMPS[(host_arch, target_arch)]
         batfilename = os.path.join(batfiledir, batfile)
         vcdir = pdir
-    elif 141 > vernum_int >= 80:
-        # 14.0 (VS2015) to 8.0 (VS2005)
-        arg, cl_path_comps = _LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS[(host_arch, target_arch)]
+    elif 141 > vernum_int >= 100:
+        # 14.0 (VS2015) to 10.0 (VS2010)
+        arg, batfile, cl_path_comps = _LE2015_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS[(host_arch, target_arch)]
         batfilename = os.path.join(pdir, "vcvarsall.bat")
-        if msvc_version == '9.0' and not os.path.exists(batfilename):
-            # Visual C++ for Python batch file is in installdir (root) not productdir (vc)
-            batfilename = os.path.normpath(os.path.join(pdir, os.pardir, "vcvarsall.bat"))
-            # Visual C++ for Python sdk batch files do not point to the VCForPython installation
+        depbat = os.path.join(pdir, *cl_path_comps, batfile)
+        clexe = os.path.join(pdir, *cl_path_comps, _CL_EXE_NAME)
+    elif 100 > vernum_int >= 80:
+        # 9.0 (VS2008) to 8.0 (VS2005)
+        arg, batfile, cl_path_comps = _LE2008_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS[(host_arch, target_arch)]
+        if vernum_int == 90 and MSVC.Kind.msvc_version_is_vcforpython(msvc_version):
+            # 9.0 (VS2008) Visual C++ for Python:
+            #     sdk batch files do not point to the VCForPython installation
+            #     vcvarsall batch file is in installdir not productdir (e.g., vc\..\vcvarsall.bat)
+            #     dependency batch files are not called from vcvarsall.bat
             sdk_pdir = None
+            batfilename = os.path.join(pdir, os.pardir, "vcvarsall.bat")
+            depbat = None
+        else:
+            batfilename = os.path.join(pdir, "vcvarsall.bat")
+            depbat = os.path.join(pdir, *cl_path_comps, batfile)
         clexe = os.path.join(pdir, *cl_path_comps, _CL_EXE_NAME)
     else:  # 80 > vernum_int
         # 7.1 (VS2003) and earlier
@@ -974,7 +1813,11 @@ def find_batch_file(msvc_version, host_arch, target_arch, pdir):
         batfilename = None
 
     if clexe and not os.path.exists(clexe):
-        debug("cl.exe not found: %s", clexe)
+        debug("%s not found: %s", _CL_EXE_NAME, clexe)
+        batfilename = None
+
+    if depbat and not os.path.exists(depbat):
+        debug("dependency batch file not found: %s", depbat)
         batfilename = None
 
     return batfilename, arg, vcdir, sdk_pdir
@@ -999,16 +1842,23 @@ def find_batch_file_sdk(host_arch, target_arch, sdk_pdir):
     return None
 
 __INSTALLED_VCS_RUN = None
+
+def _reset_installed_vcs():
+    global __INSTALLED_VCS_RUN
+    debug('reset __INSTALLED_VCS_RUN')
+    __INSTALLED_VCS_RUN = None
+
 _VC_TOOLS_VERSION_FILE_PATH = ['Auxiliary', 'Build', 'Microsoft.VCToolsVersion.default.txt']
 _VC_TOOLS_VERSION_FILE = os.sep.join(_VC_TOOLS_VERSION_FILE_PATH)
 
-def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool:
-    """Return status of finding a cl.exe to use.
+def _check_files_exist_in_vc_dir(env, vc_dir, msvc_version) -> bool:
+    """Return status of finding batch file and cl.exe to use.
 
-    Locates cl in the vc_dir depending on TARGET_ARCH, HOST_ARCH and the
-    msvc version. TARGET_ARCH and HOST_ARCH can be extracted from the
-    passed env, unless the env is None, in which case the native platform is
-    assumed for the host and all associated targets.
+    Locates required vcvars batch files and cl in the vc_dir depending on
+    TARGET_ARCH, HOST_ARCH and the msvc version. TARGET_ARCH and HOST_ARCH
+    can be extracted from the passed env, unless the env is None, in which
+    case the native platform is assumed for the host and all associated
+    targets.
 
     Args:
         env: Environment
@@ -1067,33 +1917,72 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool:
                 debug('unsupported host/target platform combo: (%s,%s)', host_platform, target_platform)
                 continue
 
-            _, cl_path_comps = batchfile_clpathcomps
+            batfile, cl_path_comps = batchfile_clpathcomps
+
+            batfile_path = os.path.join(vc_dir, "Auxiliary", "Build", batfile)
+            if not os.path.exists(batfile_path):
+                debug("batch file not found: %s", batfile_path)
+                continue
+
             cl_path = os.path.join(vc_dir, 'Tools', 'MSVC', vc_specific_version, *cl_path_comps, _CL_EXE_NAME)
-            debug('checking for %s at %s', _CL_EXE_NAME, cl_path)
+            if not os.path.exists(cl_path):
+                debug("%s not found: %s", _CL_EXE_NAME, cl_path)
+                continue
 
-            if os.path.exists(cl_path):
-                debug('found %s!', _CL_EXE_NAME)
-                return True
+            debug('%s found: %s', _CL_EXE_NAME, cl_path)
+            return True
 
     elif 141 > vernum_int >= 80:
         # 14.0 (VS2015) to 8.0 (VS2005)
 
+        if vernum_int >= 100:
+            # 14.0 (VS2015) to 10.0 (VS2010)
+            host_target_batcharg_batchfile_clpathcomps = _LE2015_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS
+        else:
+            # 9.0 (VS2008) to 8.0 (VS2005)
+            host_target_batcharg_batchfile_clpathcomps = _LE2008_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS
+
+        if vernum_int == 90 and MSVC.Kind.msvc_version_is_vcforpython(msvc_version):
+            # 9.0 (VS2008) Visual C++ for Python:
+            #     vcvarsall batch file is in installdir not productdir (e.g., vc\..\vcvarsall.bat)
+            #     dependency batch files are not called from vcvarsall.bat
+            batfile_path = os.path.join(vc_dir, os.pardir, "vcvarsall.bat")
+            check_depbat = False
+        else:
+            batfile_path = os.path.join(vc_dir, "vcvarsall.bat")
+            check_depbat = True
+
+        if not os.path.exists(batfile_path):
+            debug("batch file not found: %s", batfile_path)
+            return False
+
         for host_platform, target_platform in host_target_list:
 
             debug('host platform %s, target platform %s for version %s', host_platform, target_platform, msvc_version)
 
-            batcharg_clpathcomps = _LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS.get((host_platform, target_platform), None)
-            if batcharg_clpathcomps is None:
+            batcharg_batchfile_clpathcomps = host_target_batcharg_batchfile_clpathcomps.get(
+                (host_platform, target_platform), None
+            )
+
+            if batcharg_batchfile_clpathcomps is None:
                 debug('unsupported host/target platform combo: (%s,%s)', host_platform, target_platform)
                 continue
 
-            _, cl_path_comps = batcharg_clpathcomps
+            _, batfile, cl_path_comps = batcharg_batchfile_clpathcomps
+
+            if check_depbat:
+                batfile_path = os.path.join(vc_dir, *cl_path_comps, batfile)
+                if not os.path.exists(batfile_path):
+                    debug("batch file not found: %s", batfile_path)
+                    continue
+
             cl_path = os.path.join(vc_dir, *cl_path_comps, _CL_EXE_NAME)
-            debug('checking for %s at %s', _CL_EXE_NAME, cl_path)
+            if not os.path.exists(cl_path):
+                debug("%s not found: %s", _CL_EXE_NAME, cl_path)
+                continue
 
-            if os.path.exists(cl_path):
-                debug('found %s', _CL_EXE_NAME)
-                return True
+            debug('%s found: %s', _CL_EXE_NAME, cl_path)
+            return True
 
     elif 80 > vernum_int >= 60:
         # 7.1 (VS2003) to 6.0 (VS6)
@@ -1110,7 +1999,7 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool:
             for cl_dir in cl_dirs:
                 cl_path = os.path.join(cl_root, cl_dir, _CL_EXE_NAME)
                 if os.path.exists(cl_path):
-                    debug('%s found %s', _CL_EXE_NAME, cl_path)
+                    debug('%s found: %s', _CL_EXE_NAME, cl_path)
                     return True
         return False
 
@@ -1123,6 +2012,8 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool:
 def get_installed_vcs(env=None):
     global __INSTALLED_VCS_RUN
 
+    _VSWhereExecutable.vswhere_freeze_env(env)
+
     if __INSTALLED_VCS_RUN is not None:
         return __INSTALLED_VCS_RUN
 
@@ -1138,17 +2029,17 @@ def get_installed_vcs(env=None):
     for ver in _VCVER:
         debug('trying to find VC %s', ver)
         try:
-            VC_DIR = find_vc_pdir(env, ver)
+            VC_DIR = find_vc_pdir(ver, env)
             if VC_DIR:
                 debug('found VC %s', ver)
-                if _check_cl_exists_in_vc_dir(env, VC_DIR, ver):
+                if _check_files_exist_in_vc_dir(env, VC_DIR, ver):
                     installed_versions.append(ver)
                 else:
                     debug('no compiler found %s', ver)
             else:
                 debug('return None for ver %s', ver)
-        except (MSVCUnsupportedTargetArch, MSVCUnsupportedHostArch):
-            # Allow this exception to propagate further as it should cause
+        except (MSVCUnsupportedTargetArch, MSVCUnsupportedHostArch, VSWhereUserError):
+            # Allow these exceptions to propagate further as it should cause
             # SCons to exit with an error code
             raise
         except VisualCException as e:
@@ -1164,8 +2055,10 @@ def get_installed_vcs(env=None):
 
 def reset_installed_vcs() -> None:
     """Make it try again to find VC.  This is just for the tests."""
-    global __INSTALLED_VCS_RUN
-    __INSTALLED_VCS_RUN = None
+    _reset_installed_vcs()
+    _reset_vc_pdir()
+    _VSWhereExecutable.reset()
+    _VSWhere.reset()
     MSVC._reset()
 
 def msvc_default_version(env=None):
@@ -1341,7 +2234,7 @@ def msvc_find_valid_batch_script(env, version):
     # Find the product directory
     pdir = None
     try:
-        pdir = find_vc_pdir(env, version)
+        pdir = find_vc_pdir(version, env)
     except UnsupportedVersion:
         # Unsupported msvc version (raise MSVCArgumentError?)
         pass
@@ -1386,6 +2279,13 @@ def msvc_find_valid_batch_script(env, version):
             if not vc_script:
                 continue
 
+            if not target_platform and MSVC.ScriptArguments.msvc_script_arguments_has_uwp(env):
+                # no target arch specified and is a store/uwp build
+                if MSVC.Kind.msvc_version_skip_uwp_target(env, version):
+                    # store/uwp may not be supported for all express targets (prevent constraint error)
+                    debug('skip uwp target arch: version=%s, target=%s', repr(version), repr(target_arch))
+                    continue
+
             # Try to use the located batch file for this host/target platform combo
             arg = MSVC.ScriptArguments.msvc_script_arguments(env, version, vc_dir, arg)
             debug('trying vc_script:%s, vc_script_args:%s', repr(vc_script), arg)
@@ -1483,6 +2383,9 @@ def get_use_script_use_settings(env):
 
 def msvc_setup_env(env):
     debug('called')
+
+    _VSWhereExecutable.vswhere_freeze_env(env)
+
     version = get_default_version(env)
     if version is None:
         if not msvc_setup_env_user(env):
@@ -1616,13 +2519,17 @@ def msvc_sdk_versions(version=None, msvc_uwp_app: bool=False):
         msg = f'Unsupported version {version!r}'
         raise MSVCArgumentError(msg)
 
-    rval = MSVC.WinSDK.get_msvc_sdk_version_list(version, msvc_uwp_app)
+    rval = MSVC.WinSDK.get_msvc_sdk_version_list(version_def.msvc_version, msvc_uwp_app)
     return rval
 
-def msvc_toolset_versions(msvc_version=None, full: bool=True, sxs: bool=False):
-    debug('msvc_version=%s, full=%s, sxs=%s', repr(msvc_version), repr(full), repr(sxs))
+def msvc_toolset_versions(msvc_version=None, full: bool=True, sxs: bool=False, vswhere_exe=None):
+    debug(
+        'msvc_version=%s, full=%s, sxs=%s, vswhere_exe=%s',
+        repr(msvc_version), repr(full), repr(sxs), repr(vswhere_exe)
+    )
+
+    _VSWhereExecutable.vswhere_freeze_executable(vswhere_exe)
 
-    env = None
     rval = []
 
     if not msvc_version:
@@ -1636,7 +2543,7 @@ def msvc_toolset_versions(msvc_version=None, full: bool=True, sxs: bool=False):
         msg = f'Unsupported msvc version {msvc_version!r}'
         raise MSVCArgumentError(msg)
 
-    vc_dir = find_vc_pdir(env, msvc_version)
+    vc_dir = _find_vc_pdir(msvc_version, vswhere_exe)
     if not vc_dir:
         debug('VC folder not found for version %s', repr(msvc_version))
         return rval
@@ -1644,10 +2551,11 @@ def msvc_toolset_versions(msvc_version=None, full: bool=True, sxs: bool=False):
     rval = MSVC.ScriptArguments._msvc_toolset_versions_internal(msvc_version, vc_dir, full=full, sxs=sxs)
     return rval
 
-def msvc_toolset_versions_spectre(msvc_version=None):
-    debug('msvc_version=%s', repr(msvc_version))
+def msvc_toolset_versions_spectre(msvc_version=None, vswhere_exe=None):
+    debug('msvc_version=%s, vswhere_exe=%s', repr(msvc_version), repr(vswhere_exe))
+
+    _VSWhereExecutable.vswhere_freeze_executable(vswhere_exe)
 
-    env = None
     rval = []
 
     if not msvc_version:
@@ -1661,7 +2569,7 @@ def msvc_toolset_versions_spectre(msvc_version=None):
         msg = f'Unsupported msvc version {msvc_version!r}'
         raise MSVCArgumentError(msg)
 
-    vc_dir = find_vc_pdir(env, msvc_version)
+    vc_dir = _find_vc_pdir(msvc_version, vswhere_exe)
     if not vc_dir:
         debug('VC folder not found for version %s', repr(msvc_version))
         return rval
@@ -1669,30 +2577,91 @@ def msvc_toolset_versions_spectre(msvc_version=None):
     rval = MSVC.ScriptArguments._msvc_toolset_versions_spectre_internal(msvc_version, vc_dir)
     return rval
 
-def msvc_query_version_toolset(version=None, prefer_newest: bool=True):
+_InstalledVersionToolset = namedtuple('_InstalledVersionToolset', [
+    'msvc_version_def',
+    'toolset_version_def',
+])
+
+_InstalledVCSToolsetsComponents = namedtuple('_InstalledVCSToolsetComponents', [
+    'sxs_map',
+    'toolset_vcs',
+    'msvc_toolset_component_defs',
+])
+
+def get_installed_vcs_toolsets_components(vswhere_exe=None):
+    debug('vswhere_exe=%s', repr(vswhere_exe))
+
+    _VSWhereExecutable.vswhere_freeze_executable(vswhere_exe)
+
+    sxs_map = {}
+    toolset_vcs = set()
+    msvc_toolset_component_defs = []
+
+    vcs = get_installed_vcs()
+    for msvc_version in vcs:
+
+        vc_dir = _find_vc_pdir(msvc_version, vswhere_exe)
+        if not vc_dir:
+            continue
+
+        msvc_version_def = MSVC.Util.msvc_version_components(msvc_version)
+        toolset_vcs.add(msvc_version_def.msvc_version)
+
+        if msvc_version_def.msvc_vernum > 14.0:
+
+            toolsets_sxs, toolsets_full = MSVC.ScriptArguments._msvc_version_toolsets_internal(
+                msvc_version, vc_dir
+            )
+
+            debug('msvc_version=%s, toolset_sxs=%s', repr(msvc_version), repr(toolsets_sxs))
+            sxs_map.update(toolsets_sxs)
+
+        else:
+
+            toolsets_full = [msvc_version_def.msvc_verstr]
+
+        for toolset_version in toolsets_full:
+            debug('msvc_version=%s, toolset_version=%s', repr(msvc_version), repr(toolset_version))
+
+            toolset_version_def = MSVC.Util.msvc_extended_version_components(toolset_version)
+            if not toolset_version_def:
+                continue
+            toolset_vcs.add(toolset_version_def.msvc_version)
+
+            rval = _InstalledVersionToolset(
+                msvc_version_def=msvc_version_def,
+                toolset_version_def=toolset_version_def,
+            )
+            msvc_toolset_component_defs.append(rval)
+
+    installed_vcs_toolsets_components = _InstalledVCSToolsetsComponents(
+        sxs_map=sxs_map,
+        toolset_vcs=toolset_vcs,
+        msvc_toolset_component_defs=msvc_toolset_component_defs,
+    )
+
+    return installed_vcs_toolsets_components
+
+def msvc_query_version_toolset(version=None, prefer_newest: bool=True, vswhere_exe=None):
     """
-    Returns an msvc version and a toolset version given a version
+    Return an msvc version and a toolset version given a version
     specification.
 
     This is an EXPERIMENTAL proxy for using a toolset version to perform
     msvc instance selection.  This function will be removed when
     toolset version is taken into account during msvc instance selection.
 
-    Search for an installed Visual Studio instance that supports the
-    specified version.
+    This function searches for an installed Visual Studio instance that
+    contains the requested version. A component suffix (e.g., Exp) is not
+    supported for toolset version specifications (e.g., 14.39).
 
-    When the specified version contains a component suffix (e.g., Exp),
-    the msvc version is returned and the toolset version is None. No
-    search if performed.
+    An MSVCArgumentError is raised when argument validation fails. An
+    MSVCToolsetVersionNotFound exception is raised when the requested
+    version is not found.
 
-    When the specified version does not contain a component suffix, the
-    version is treated as a toolset version specification. A search is
-    performed for the first msvc instance that contains the toolset
-    version.
-
-    Only Visual Studio 2017 and later support toolset arguments.  For
-    Visual Studio 2015 and earlier, the msvc version is returned and
-    the toolset version is None.
+    For Visual Studio 2015 and earlier, the msvc version is returned and
+    the toolset version is None.  For Visual Studio 2017 and later, the
+    selected toolset version is returned.
 
     Args:
 
@@ -1706,6 +2675,9 @@ def msvc_query_version_toolset(version=None, prefer_newest: bool=True):
                    the native Visual Studio instance is not detected, prefer
                    newer Visual Studio instances.
 
+        vswhere_exe: str
+            A vswhere executable location or None.
+
     Returns:
         tuple: A tuple containing the msvc version and the msvc toolset version.
                The msvc toolset version may be None.
@@ -1714,112 +2686,140 @@ def msvc_query_version_toolset(version=None, prefer_newest: bool=True):
         MSVCToolsetVersionNotFound: when the specified version is not found.
         MSVCArgumentError: when argument validation fails.
     """
-    debug('version=%s, prefer_newest=%s', repr(version), repr(prefer_newest))
+    debug(
+        'version=%s, prefer_newest=%s, vswhere_exe=%s',
+        repr(version), repr(prefer_newest), repr(vswhere_exe)
+    )
+
+    _VSWhereExecutable.vswhere_freeze_executable(vswhere_exe)
 
-    env = None
     msvc_version = None
     msvc_toolset_version = None
 
-    if not version:
-        version = msvc_default_version()
+    with MSVC.Policy.msvc_notfound_policy_contextmanager('suppress'):
 
-    if not version:
-        debug('no msvc versions detected')
-        return msvc_version, msvc_toolset_version
+        vcs = get_installed_vcs()
 
-    version_def = MSVC.Util.msvc_extended_version_components(version)
+        if not version:
+            version = msvc_default_version()
 
-    if not version_def:
-        msg = f'Unsupported msvc version {version!r}'
-        raise MSVCArgumentError(msg)
+        if not version:
+            msg = f'No versions of the MSVC compiler were found'
+            debug(f'MSVCToolsetVersionNotFound: {msg}')
+            raise MSVCToolsetVersionNotFound(msg)
 
-    if version_def.msvc_suffix:
-        if version_def.msvc_verstr != version_def.msvc_toolset_version:
-            # toolset version with component suffix
-            msg = f'Unsupported toolset version {version!r}'
-            raise MSVCArgumentError(msg)
+        version_def = MSVC.Util.msvc_extended_version_components(version)
 
-    if version_def.msvc_vernum > 14.0:
-        # VS2017 and later
-        force_toolset_msvc_version = False
-    else:
-        # VS2015 and earlier
-        force_toolset_msvc_version = True
-        extended_version = version_def.msvc_verstr + '0.00000'
-        if not extended_version.startswith(version_def.msvc_toolset_version):
-            # toolset not equivalent to msvc version
-            msg = 'Unsupported toolset version {} (expected {})'.format(
-                repr(version), repr(extended_version)
-            )
+        if not version_def:
+            msg = f'Unsupported MSVC version {version!r}'
+            debug(f'MSVCArgumentError: {msg}')
             raise MSVCArgumentError(msg)
 
-    msvc_version = version_def.msvc_version
+        msvc_version = version_def.msvc_version
 
-    if msvc_version not in MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP:
-        # VS2013 and earlier
-        debug(
-            'ignore: msvc_version=%s, msvc_toolset_version=%s',
-            repr(msvc_version), repr(msvc_toolset_version)
-        )
-        return msvc_version, msvc_toolset_version
+        if version_def.msvc_suffix:
 
-    if force_toolset_msvc_version:
-        query_msvc_toolset_version = version_def.msvc_verstr
-    else:
-        query_msvc_toolset_version = version_def.msvc_toolset_version
+            if version_def.msvc_verstr != version_def.msvc_toolset_version:
+                # toolset version with component suffix
+                msg = f'Unsupported MSVC toolset version {version!r}'
+                debug(f'MSVCArgumentError: {msg}')
+                raise MSVCArgumentError(msg)
 
-    if prefer_newest:
-        query_version_list = MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP[msvc_version]
-    else:
-        query_version_list = MSVC.Config.MSVC_VERSION_TOOLSET_DEFAULTS_MAP[msvc_version] + \
-                             MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP[msvc_version]
+            if msvc_version not in vcs:
+                msg = f'MSVC version {msvc_version!r} not found'
+                debug(f'MSVCToolsetVersionNotFound: {msg}')
+                raise MSVCToolsetVersionNotFound(msg)
+
+            if msvc_version not in MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP:
+                debug(
+                    'suffix: msvc_version=%s, msvc_toolset_version=%s',
+                    repr(msvc_version), repr(msvc_toolset_version)
+                )
+                return msvc_version, msvc_toolset_version
 
-    seen_msvc_version = set()
-    for query_msvc_version in query_version_list:
+        if version_def.msvc_vernum > 14.0:
+            # VS2017 and later
+            force_toolset_msvc_version = False
+        else:
+            # VS2015 and earlier
+            force_toolset_msvc_version = True
+            extended_version = version_def.msvc_verstr + '0.00000'
+            if not extended_version.startswith(version_def.msvc_toolset_version):
+                # toolset not equivalent to msvc version
+                msg = 'Unsupported MSVC toolset version {} (expected {})'.format(
+                    repr(version), repr(extended_version)
+                )
+                debug(f'MSVCArgumentError: {msg}')
+                raise MSVCArgumentError(msg)
 
-        if query_msvc_version in seen_msvc_version:
-            continue
-        seen_msvc_version.add(query_msvc_version)
+        if msvc_version not in MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP:
 
-        vc_dir = find_vc_pdir(env, query_msvc_version)
-        if not vc_dir:
-            continue
+            # VS2013 and earlier
+
+            if msvc_version not in vcs:
+                msg = f'MSVC version {version!r} not found'
+                debug(f'MSVCToolsetVersionNotFound: {msg}')
+                raise MSVCToolsetVersionNotFound(msg)
 
-        if query_msvc_version.startswith('14.0'):
-            # VS2015 does not support toolset version argument
-            msvc_toolset_version = None
             debug(
-                'found: msvc_version=%s, msvc_toolset_version=%s',
-                repr(query_msvc_version), repr(msvc_toolset_version)
+                'earlier: msvc_version=%s, msvc_toolset_version=%s',
+                repr(msvc_version), repr(msvc_toolset_version)
             )
-            return query_msvc_version, msvc_toolset_version
+            return msvc_version, msvc_toolset_version
 
-        try:
-            toolset_vcvars = MSVC.ScriptArguments._msvc_toolset_internal(query_msvc_version, query_msvc_toolset_version, vc_dir)
-            if toolset_vcvars:
-                msvc_toolset_version = toolset_vcvars
+        if force_toolset_msvc_version:
+            query_msvc_toolset_version = version_def.msvc_verstr
+        else:
+            query_msvc_toolset_version = version_def.msvc_toolset_version
+
+        if prefer_newest:
+            query_version_list = MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP[msvc_version]
+        else:
+            query_version_list = MSVC.Config.MSVC_VERSION_TOOLSET_DEFAULTS_MAP[msvc_version] + \
+                                 MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP[msvc_version]
+
+        seen_msvc_version = set()
+        for query_msvc_version in query_version_list:
+
+            if query_msvc_version in seen_msvc_version:
+                continue
+            seen_msvc_version.add(query_msvc_version)
+
+            vc_dir = _find_vc_pdir(query_msvc_version, vswhere_exe)
+            if not vc_dir:
+                continue
+
+            if query_msvc_version.startswith('14.0'):
+                # VS2015 does not support toolset version argument
+                msvc_toolset_version = None
                 debug(
                     'found: msvc_version=%s, msvc_toolset_version=%s',
                     repr(query_msvc_version), repr(msvc_toolset_version)
                 )
                 return query_msvc_version, msvc_toolset_version
 
-        except MSVCToolsetVersionNotFound:
-            pass
+            try:
+                toolset_vcvars = MSVC.ScriptArguments._msvc_toolset_internal(query_msvc_version, query_msvc_toolset_version, vc_dir)
+                if toolset_vcvars:
+                    msvc_toolset_version = toolset_vcvars
+                    debug(
+                        'found: msvc_version=%s, msvc_toolset_version=%s',
+                        repr(query_msvc_version), repr(msvc_toolset_version)
+                    )
+                    return query_msvc_version, msvc_toolset_version
+
+            except MSVCToolsetVersionNotFound:
+                pass
 
-    msvc_toolset_version = query_msvc_toolset_version
+        msvc_toolset_version = query_msvc_toolset_version
 
     debug(
         'not found: msvc_version=%s, msvc_toolset_version=%s',
         repr(msvc_version), repr(msvc_toolset_version)
     )
 
-    if version_def.msvc_verstr == msvc_toolset_version:
-        msg = f'MSVC version {version!r} was not found'
-        MSVC.Policy.msvc_notfound_handler(None, msg)
-        return msvc_version, msvc_toolset_version
-
     msg = f'MSVC toolset version {version!r} not found'
+    debug(f'MSVCToolsetVersionNotFound: {msg}')
     raise MSVCToolsetVersionNotFound(msg)
 
 
diff --git a/SCons/Tool/MSCommon/vcTests.py b/SCons/Tool/MSCommon/vcTests.py
index 9c2bd0ac8c..e1146e89be 100644
--- a/SCons/Tool/MSCommon/vcTests.py
+++ b/SCons/Tool/MSCommon/vcTests.py
@@ -57,12 +57,22 @@ def testDefaults(self) -> None:
         Verify that msvc_find_vswhere() find's files in the specified paths
         """
         # import pdb; pdb.set_trace()
-        vswhere_dirs = [os.path.splitdrive(p)[1] for p in SCons.Tool.MSCommon.vc.VSWHERE_PATHS]
+        test_all_dirs = []
+
         base_dir = test.workpath('fake_vswhere')
+
+        vswhere_dirs = [os.path.splitdrive(p)[1] for p in SCons.Tool.MSCommon.vc._VSWHERE_EXEGROUP_MSVS]
+        test_vswhere_dirs = [os.path.join(base_dir,d[1:]) for d in vswhere_dirs]
+        SCons.Tool.MSCommon.vc._VSWHERE_EXEGROUP_MSVS = test_vswhere_dirs
+        test_all_dirs += test_vswhere_dirs
+
+        vswhere_dirs = [os.path.splitdrive(p)[1] for p in SCons.Tool.MSCommon.vc._VSWHERE_EXEGROUP_PKGMGR]
         test_vswhere_dirs = [os.path.join(base_dir,d[1:]) for d in vswhere_dirs]
+        SCons.Tool.MSCommon.vc._VSWHERE_EXEGROUP_PKGMGR = test_vswhere_dirs
+        test_all_dirs += test_vswhere_dirs
 
-        SCons.Tool.MSCommon.vc.VSWHERE_PATHS = test_vswhere_dirs
-        for vsw in test_vswhere_dirs:
+        for vsw in test_all_dirs:
+            SCons.Tool.MSCommon.vc._VSWhereExecutable.reset()
             VswhereTestCase._createVSWhere(vsw)
             find_path = SCons.Tool.MSCommon.vc.msvc_find_vswhere()
             self.assertTrue(vsw == find_path, "Didn't find vswhere in %s found in %s" % (vsw, find_path))
@@ -75,7 +85,7 @@ def testDefaults(self) -> None:
 class MSVcTestCase(unittest.TestCase):
 
     @staticmethod
-    def _createDummyCl(path, add_bin: bool=True) -> None:
+    def _createDummyFile(path, filename, add_bin: bool=True) -> None:
         """
         Creates a dummy cl.exe in the correct directory.
         It will create all missing parent directories as well
@@ -94,7 +104,7 @@ def _createDummyCl(path, add_bin: bool=True) -> None:
         if create_path and not os.path.isdir(create_path):
             os.makedirs(create_path)
 
-        create_this = os.path.join(create_path,'cl.exe')
+        create_this = os.path.join(create_path, filename)
 
         # print("Creating: %s"%create_this)
         with open(create_this,'w') as ct:
@@ -105,18 +115,10 @@ def runTest(self) -> None:
         """
         Check that all proper HOST_PLATFORM and TARGET_PLATFORM are handled.
         Verify that improper HOST_PLATFORM and/or TARGET_PLATFORM are properly handled.
-        by SCons.Tool.MSCommon.vc._check_cl_exists_in_vc_dir()
+        by SCons.Tool.MSCommon.vc._check_files_exist_in_vc_dir()
         """
 
-        check = SCons.Tool.MSCommon.vc._check_cl_exists_in_vc_dir
-
-        env={'TARGET_ARCH':'x86'}
-        _, clpathcomps = SCons.Tool.MSCommon.vc._LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS[('x86','x86')]
-        path = os.path.join('.', *clpathcomps)
-        MSVcTestCase._createDummyCl(path, add_bin=False)
-
-        # print("retval:%s"%check(env, '.', '8.0'))
-
+        check = SCons.Tool.MSCommon.vc._check_files_exist_in_vc_dir
 
         # Setup for 14.1 (VS2017) and later tests
 
@@ -131,122 +133,70 @@ def runTest(self) -> None:
         except IOError as e:
             print("Failed trying to write :%s :%s" % (tools_version_file, e))
 
-
         # Test 14.3 (VS2022) and later
         vc_ge2022_list = SCons.Tool.MSCommon.vc._GE2022_HOST_TARGET_CFG.all_pairs
-
         for host, target in vc_ge2022_list:
             batfile, clpathcomps = SCons.Tool.MSCommon.vc._GE2022_HOST_TARGET_BATCHFILE_CLPATHCOMPS[(host,target)]
             # print("GE 14.3 Got: (%s, %s) -> (%s, %s)"%(host,target,batfile,clpathcomps))
 
             env={'TARGET_ARCH':target, 'HOST_ARCH':host}
+            path = os.path.join('.', "Auxiliary", "Build", batfile)
+            MSVcTestCase._createDummyFile(path, batfile, add_bin=False)
             path = os.path.join('.', 'Tools', 'MSVC', MS_TOOLS_VERSION, *clpathcomps)
-            MSVcTestCase._createDummyCl(path, add_bin=False)
+            MSVcTestCase._createDummyFile(path, 'cl.exe', add_bin=False)
             result=check(env, '.', '14.3')
             # print("for:(%s, %s) got :%s"%(host, target, result))
             self.assertTrue(result, "Checking host: %s target: %s" % (host, target))
 
-        # Now test bogus value for HOST_ARCH
-        env={'TARGET_ARCH':'x86', 'HOST_ARCH':'GARBAGE'}
-        try:
-            result=check(env, '.', '14.3')
-            # print("for:%s got :%s"%(env, result))
-            self.assertFalse(result, "Did not fail with bogus HOST_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH']))
-        except MSVCUnsupportedHostArch:
-            pass
-        else:
-            self.fail('Did not fail when HOST_ARCH specified as: %s' % env['HOST_ARCH'])
-
-        # Now test bogus value for TARGET_ARCH
-        env={'TARGET_ARCH':'GARBAGE', 'HOST_ARCH':'x86'}
-        try:
-            result=check(env, '.', '14.3')
-            # print("for:%s got :%s"%(env, result))
-            self.assertFalse(result, "Did not fail with bogus TARGET_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH']))
-        except MSVCUnsupportedTargetArch:
-            pass
-        else:
-            self.fail('Did not fail when TARGET_ARCH specified as: %s' % env['TARGET_ARCH'])
-
         # Test 14.2 (VS2019) to 14.1 (VS2017) versions
         vc_le2019_list = SCons.Tool.MSCommon.vc._LE2019_HOST_TARGET_CFG.all_pairs
-
         for host, target in vc_le2019_list:
             batfile, clpathcomps = SCons.Tool.MSCommon.vc._LE2019_HOST_TARGET_BATCHFILE_CLPATHCOMPS[(host,target)]
             # print("LE 14.2 Got: (%s, %s) -> (%s, %s)"%(host,target,batfile,clpathcomps))
 
             env={'TARGET_ARCH':target, 'HOST_ARCH':host}
             path = os.path.join('.', 'Tools', 'MSVC', MS_TOOLS_VERSION, *clpathcomps)
-            MSVcTestCase._createDummyCl(path, add_bin=False)
+            MSVcTestCase._createDummyFile(path, 'cl.exe', add_bin=False)
             result=check(env, '.', '14.1')
             # print("for:(%s, %s) got :%s"%(host, target, result))
             self.assertTrue(result, "Checking host: %s target: %s" % (host, target))
 
-        # Now test bogus value for HOST_ARCH
-        env={'TARGET_ARCH':'x86', 'HOST_ARCH':'GARBAGE'}
-        try:
-            result=check(env, '.', '14.1')
-            # print("for:%s got :%s"%(env, result))
-            self.assertFalse(result, "Did not fail with bogus HOST_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH']))
-        except MSVCUnsupportedHostArch:
-            pass
-        else:
-            self.fail('Did not fail when HOST_ARCH specified as: %s' % env['HOST_ARCH'])
-
-        # Now test bogus value for TARGET_ARCH
-        env={'TARGET_ARCH':'GARBAGE', 'HOST_ARCH':'x86'}
-        try:
-            result=check(env, '.', '14.1')
-            # print("for:%s got :%s"%(env, result))
-            self.assertFalse(result, "Did not fail with bogus TARGET_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH']))
-        except MSVCUnsupportedTargetArch:
-            pass
-        else:
-            self.fail('Did not fail when TARGET_ARCH specified as: %s' % env['TARGET_ARCH'])
-
-        # Test 14.0 (VS2015) to 8.0 (VS2005) versions
+        # Test 14.0 (VS2015) to 10.0 (VS2010) versions
         vc_le2015_list = SCons.Tool.MSCommon.vc._LE2015_HOST_TARGET_CFG.all_pairs
-
         for host, target in vc_le2015_list:
-            batarg, clpathcomps = SCons.Tool.MSCommon.vc._LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS[(host, target)]
-            # print("LE 14.0 Got: (%s, %s) -> (%s, %s)"%(host,target,batarg,clpathcomps))
+            batarg, batfile, clpathcomps = SCons.Tool.MSCommon.vc._LE2015_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS[(host, target)]
+            # print("LE 14.0 Got: (%s, %s) -> (%s, %s, %s)"%(host,target,batarg,batfile,clpathcomps))
             env={'TARGET_ARCH':target, 'HOST_ARCH':host}
+            MSVcTestCase._createDummyFile('.', 'vcvarsall.bat', add_bin=False)
             path = os.path.join('.', *clpathcomps)
-            MSVcTestCase._createDummyCl(path, add_bin=False)
-            result=check(env, '.', '9.0')
+            MSVcTestCase._createDummyFile(path, batfile, add_bin=False)
+            MSVcTestCase._createDummyFile(path, 'cl.exe', add_bin=False)
+            result=check(env, '.', '10.0')
             # print("for:(%s, %s) got :%s"%(host, target, result))
             self.assertTrue(result, "Checking host: %s target: %s" % (host, target))
 
-        # Now test bogus value for HOST_ARCH
-        env={'TARGET_ARCH':'x86', 'HOST_ARCH':'GARBAGE'}
-        try:
-            result=check(env, '.', '9.0')
-            # print("for:%s got :%s"%(env, result))
-            self.assertFalse(result, "Did not fail with bogus HOST_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH']))
-        except MSVCUnsupportedHostArch:
-            pass
-        else:
-            self.fail('Did not fail when HOST_ARCH specified as: %s' % env['HOST_ARCH'])
-
-        # Now test bogus value for TARGET_ARCH
-        env={'TARGET_ARCH':'GARBAGE', 'HOST_ARCH':'x86'}
-        try:
-            result=check(env, '.', '9.0')
-            # print("for:%s got :%s"%(env, result))
-            self.assertFalse(result, "Did not fail with bogus TARGET_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH']))
-        except MSVCUnsupportedTargetArch:
-            pass
-        else:
-            self.fail('Did not fail when TARGET_ARCH specified as: %s' % env['TARGET_ARCH'])
+        # Test 9.0 (VC2008) to 8.0 (VS2005)
+        vc_le2008_list = SCons.Tool.MSCommon.vc._LE2008_HOST_TARGET_CFG.all_pairs
+        for host, target in vc_le2008_list:
+            batarg, batfile, clpathcomps = SCons.Tool.MSCommon.vc._LE2008_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS[(host, target)]
+            # print("LE 9.0 Got: (%s, %s) -> (%s, %s, %s)"%(host,target,batarg,batfile,clpathcomps))
+            env={'TARGET_ARCH':target, 'HOST_ARCH':host}
+            MSVcTestCase._createDummyFile('.', 'vcvarsall.bat', add_bin=False)
+            path = os.path.join('.', *clpathcomps)
+            MSVcTestCase._createDummyFile(path, batfile, add_bin=False)
+            MSVcTestCase._createDummyFile(path, 'cl.exe', add_bin=False)
+            # check will fail if '9.0' and VCForPython (layout different)
+            result=check(env, '.', '8.0')
+            # print("for:(%s, %s) got :%s"%(host, target, result))
+            self.assertTrue(result, "Checking host: %s target: %s" % (host, target))
 
         # Test 7.1 (VS2003) and earlier
         vc_le2003_list = SCons.Tool.MSCommon.vc._LE2003_HOST_TARGET_CFG.all_pairs
-
         for host, target in vc_le2003_list:
             # print("LE 7.1 Got: (%s, %s)"%(host,target))
             env={'TARGET_ARCH':target, 'HOST_ARCH':host}
             path = os.path.join('.')
-            MSVcTestCase._createDummyCl(path)
+            MSVcTestCase._createDummyFile(path, 'cl.exe')
             result=check(env, '.', '6.0')
             # print("for:(%s, %s) got :%s"%(host, target, result))
             self.assertTrue(result, "Checking host: %s target: %s" % (host, target))
@@ -254,7 +204,7 @@ def runTest(self) -> None:
         # Now test bogus value for HOST_ARCH
         env={'TARGET_ARCH':'x86', 'HOST_ARCH':'GARBAGE'}
         try:
-            result=check(env, '.', '6.0')
+            result=check(env, '.', '14.3')
             # print("for:%s got :%s"%(env, result))
             self.assertFalse(result, "Did not fail with bogus HOST_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH']))
         except MSVCUnsupportedHostArch:
@@ -265,7 +215,7 @@ def runTest(self) -> None:
         # Now test bogus value for TARGET_ARCH
         env={'TARGET_ARCH':'GARBAGE', 'HOST_ARCH':'x86'}
         try:
-            result=check(env, '.', '6.0')
+            result=check(env, '.', '14.3')
             # print("for:%s got :%s"%(env, result))
             self.assertFalse(result, "Did not fail with bogus TARGET_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH']))
         except MSVCUnsupportedTargetArch:
@@ -273,12 +223,29 @@ def runTest(self) -> None:
         else:
             self.fail('Did not fail when TARGET_ARCH specified as: %s' % env['TARGET_ARCH'])
 
-
 class Data:
 
-    HAVE_MSVC = True if MSCommon.vc.msvc_default_version() else False
+    DEFAULT_VERSION = MSCommon.vc.msvc_default_version()
+
+    if DEFAULT_VERSION:
+        HAVE_MSVC = True
+        DEFAULT_VERSION_DEF = MSCommon.msvc_version_components(DEFAULT_VERSION)
+    else:
+        HAVE_MSVC = False
+        DEFAULT_VERSION_DEF = None
 
+    INSTALLED_VCS = MSCommon.vc.get_installed_vcs()
     INSTALLED_VCS_COMPONENTS = MSCommon.vc.get_installed_vcs_components()
+    INSTALLED_VCS_TOOLSETS_COMPONENTS = MSCommon.vc.get_installed_vcs_toolsets_components()
+
+    @classmethod
+    def query_version_list(cls, vcver):
+        # VS 2022 (14.3) can have either/both toolset versions 14.3X and 14.4X
+        if vcver == '14.3' or (vcver is None and cls.DEFAULT_VERSION == '14.3'):
+            vcver_list = ['14.4', '14.3']
+        else:
+            vcver_list = [vcver]
+        return vcver_list
 
     @classmethod
     def _msvc_toolset_notfound_list(cls, toolset_seen, toolset_list):
@@ -291,11 +258,11 @@ def _msvc_toolset_notfound_list(cls, toolset_seen, toolset_list):
             if len(comps) != 3:
                 continue
             # full versions only
+            ival = int(comps[-1])
             nloop = 0
             while nloop < 10:
-                ival = int(comps[-1])
                 if ival == 0:
-                    ival = 1000000
+                    ival = 100000
                 ival -= 1
                 version = '{}.{}.{:05d}'.format(comps[0], comps[1], ival)
                 if version not in toolset_seen:
@@ -371,10 +338,18 @@ def test_valid_vcver(self) -> None:
             version_def = MSCommon.msvc_version_components(symbol)
             for msvc_uwp_app in (True, False):
                 sdk_list = MSCommon.vc.msvc_sdk_versions(version=symbol, msvc_uwp_app=msvc_uwp_app)
-                if Data.HAVE_MSVC and version_def.msvc_vernum >= 14.0:
+                if version_def.msvc_vernum < 14.0:
+                    # version < VS2015
+                    # does not accept sdk version argument
+                    self.assertFalse(sdk_list, "SDK list is not empty for msvc version {}".format(repr(symbol)))
+                elif Data.HAVE_MSVC and Data.DEFAULT_VERSION_DEF.msvc_vernum >= 14.0:
+                    # version >= VS2015 and default_version >= VS2015
+                    # no guarantee test is valid: compatible sdks may not be installed as version gap grows
                     self.assertTrue(sdk_list, "SDK list is empty for msvc version {}".format(repr(symbol)))
                 else:
-                    self.assertFalse(sdk_list, "SDK list is not empty for msvc version {}".format(repr(symbol)))
+                    # version >= VS2015 and default_version < VS2015
+                    # skip test: version is not installed; compatible windows sdks may not be installed
+                    pass
 
     def test_valid_vcver_toolsets(self) -> None:
         for symbol in MSCommon.vc._VCVER:
@@ -490,15 +465,26 @@ class MsvcQueryVersionToolsetTests(unittest.TestCase):
 
     def run_valid_default_msvc(self, have_msvc) -> None:
         for prefer_newest in (True, False):
-            msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset(
-                version=None, prefer_newest=prefer_newest
-            )
-            expect = (have_msvc and msvc_version) or (not have_msvc and not msvc_version)
-            self.assertTrue(expect, "unexpected msvc_version {} for for msvc version {}".format(
+            if not have_msvc:
+                with self.assertRaises(MSCommon.vc.MSVCToolsetVersionNotFound):
+                    msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset(
+                        version=None, prefer_newest=prefer_newest
+                    )
+                continue
+            msvc_version = msvc_toolset_version = None
+            for vcver in Data.query_version_list(None):
+                try:
+                    msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset(
+                        version=vcver, prefer_newest=prefer_newest
+                    )
+                    break
+                except MSCommon.vc.MSVCToolsetVersionNotFound:
+                    pass
+            self.assertTrue(msvc_version, "unexpected msvc_version {} for for msvc version {}".format(
                 repr(msvc_version), repr(None)
             ))
             version_def = MSCommon.msvc_version_components(msvc_version)
-            if have_msvc and version_def.msvc_vernum > 14.0:
+            if version_def.msvc_vernum > 14.0:
                 # VS2017 and later for toolset version
                 self.assertTrue(msvc_toolset_version, "msvc_toolset_version is undefined for msvc version {}".format(
                     repr(None)
@@ -513,11 +499,24 @@ def test_valid_default_msvc(self) -> None:
 
     def test_valid_vcver(self) -> None:
         for symbol in MSCommon.vc._VCVER:
+            have_msvc = bool(symbol in Data.INSTALLED_VCS_TOOLSETS_COMPONENTS.toolset_vcs)
             version_def = MSCommon.msvc_version_components(symbol)
             for prefer_newest in (True, False):
-                msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset(
-                    version=symbol, prefer_newest=prefer_newest
-                )
+                if not have_msvc:
+                    with self.assertRaises(MSCommon.vc.MSVCToolsetVersionNotFound):
+                        msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset(
+                            version=symbol, prefer_newest=prefer_newest
+                        )
+                    continue
+                msvc_version = msvc_toolset_version = None
+                for vcver in Data.query_version_list(symbol):
+                    try:
+                        msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset(
+                            version=vcver, prefer_newest=prefer_newest
+                        )
+                        break
+                    except:
+                        pass
                 self.assertTrue(msvc_version, "msvc_version is undefined for msvc version {}".format(repr(symbol)))
                 if version_def.msvc_vernum > 14.0:
                     # VS2017 and later for toolset version
diff --git a/SCons/Tool/MSCommon/vs.py b/SCons/Tool/MSCommon/vs.py
index af0fd26e5a..34fb670b5f 100644
--- a/SCons/Tool/MSCommon/vs.py
+++ b/SCons/Tool/MSCommon/vs.py
@@ -28,7 +28,6 @@
 import os
 
 import SCons.Errors
-import SCons.Tool.MSCommon.vc
 import SCons.Util
 
 from .common import (
@@ -40,6 +39,19 @@
     read_reg,
 )
 
+from .vc import (
+    find_vc_pdir,
+    get_msvc_version_numeric,
+    reset_installed_vcs,
+    vswhere_freeze_env,
+)
+
+
+# Visual Studio express version policy when unqualified version is not installed:
+#     True:  use express version for unqualified version (e.g., use 12.0Exp for 12.0)
+#     False: do not use express version for unqualified version
+_VSEXPRESS_USE_VERSTR = True
+
 
 class VisualStudio:
     """
@@ -48,6 +60,9 @@ class VisualStudio:
     """
     def __init__(self, version, **kw) -> None:
         self.version = version
+        self.verstr = get_msvc_version_numeric(version)
+        self.vernum = float(self.verstr)
+        self.is_express = True if self.verstr != self.version else False
         kw['vc_version']  = kw.get('vc_version', version)
         kw['sdk_version'] = kw.get('sdk_version', version)
         self.__dict__.update(kw)
@@ -66,7 +81,7 @@ def find_batch_file(self):
         return batch_file
 
     def find_vs_dir_by_vc(self, env):
-        dir = SCons.Tool.MSCommon.vc.find_vc_pdir(env, self.vc_version)
+        dir = find_vc_pdir(self.vc_version, env)
         if not dir:
             debug('no installed VC %s', self.vc_version)
             return None
@@ -428,6 +443,7 @@ def reset(self) -> None:
 def get_installed_visual_studios(env=None):
     global InstalledVSList
     global InstalledVSMap
+    vswhere_freeze_env(env)
     if InstalledVSList is None:
         InstalledVSList = []
         InstalledVSMap = {}
@@ -436,12 +452,21 @@ def get_installed_visual_studios(env=None):
             if vs.get_executable(env):
                 debug('found VS %s', vs.version)
                 InstalledVSList.append(vs)
+                if vs.is_express and vs.verstr not in InstalledVSMap:
+                    if _VSEXPRESS_USE_VERSTR:
+                        InstalledVSMap[vs.verstr] = vs
                 InstalledVSMap[vs.version] = vs
     return InstalledVSList
 
+def _get_installed_vss(env=None):
+    get_installed_visual_studios(env)
+    versions = list(InstalledVSMap.keys())
+    return versions
+
 def reset_installed_visual_studios() -> None:
     global InstalledVSList
     global InstalledVSMap
+    debug('')
     InstalledVSList = None
     InstalledVSMap  = None
     for vs in SupportedVSList:
@@ -449,7 +474,7 @@ def reset_installed_visual_studios() -> None:
 
     # Need to clear installed VC's as well as they are used in finding
     # installed VS's
-    SCons.Tool.MSCommon.vc.reset_installed_vcs()
+    reset_installed_vcs()
 
 
 # We may be asked to update multiple construction environments with
@@ -487,7 +512,7 @@ def reset_installed_visual_studios() -> None:
 def msvs_exists(env=None) -> bool:
     return len(get_installed_visual_studios(env)) > 0
 
-def get_vs_by_version(msvs):
+def get_vs_by_version(msvs, env=None):
     global InstalledVSMap
     global SupportedVSMap
 
@@ -495,7 +520,7 @@ def get_vs_by_version(msvs):
     if msvs not in SupportedVSMap:
         msg = "Visual Studio version %s is not supported" % repr(msvs)
         raise SCons.Errors.UserError(msg)
-    get_installed_visual_studios()
+    get_installed_visual_studios(env)
     vs = InstalledVSMap.get(msvs)
     debug('InstalledVSMap:%s', InstalledVSMap)
     debug('found vs:%s', vs)
@@ -524,7 +549,7 @@ def get_default_version(env):
     """
     if 'MSVS' not in env or not SCons.Util.is_Dict(env['MSVS']):
         # get all versions, and remember them for speed later
-        versions = [vs.version for vs in get_installed_visual_studios()]
+        versions = _get_installed_vss(env)
         env['MSVS'] = {'VERSIONS' : versions}
     else:
         versions = env['MSVS'].get('VERSIONS', [])
@@ -570,7 +595,7 @@ def merge_default_version(env) -> None:
 
 # TODO: refers to versions and arch which aren't defined; called nowhere. Drop?
 def msvs_setup_env(env) -> None:
-    msvs = get_vs_by_version(version)
+    msvs = get_vs_by_version(version, env)
     if msvs is None:
         return
     batfilename = msvs.get_batch_file()
@@ -582,7 +607,7 @@ def msvs_setup_env(env) -> None:
 
         vars = ('LIB', 'LIBPATH', 'PATH', 'INCLUDE')
 
-        msvs_list = get_installed_visual_studios()
+        msvs_list = get_installed_visual_studios(env)
         vscommonvarnames = [vs.common_tools_var for vs in msvs_list]
         save_ENV = env['ENV']
         nenv = normalize_env(env['ENV'],
@@ -597,11 +622,10 @@ def msvs_setup_env(env) -> None:
         for k, v in vars.items():
             env.PrependENVPath(k, v, delete_existing=1)
 
-def query_versions():
+def query_versions(env=None):
     """Query the system to get available versions of VS. A version is
     considered when a batfile is found."""
-    msvs_list = get_installed_visual_studios()
-    versions = [msvs.version for msvs in msvs_list]
+    versions = _get_installed_vss(env)
     return versions
 
 # Local Variables:
diff --git a/SCons/Tool/msvc.py b/SCons/Tool/msvc.py
index 6afa171c97..b823752b50 100644
--- a/SCons/Tool/msvc.py
+++ b/SCons/Tool/msvc.py
@@ -296,9 +296,6 @@ def generate(env) -> None:
     # without it for lex generation
     env["LEXUNISTD"] = SCons.Util.CLVar("--nounistd")
 
-    # Get user specified vswhere location or locate.
-    env['VSWHERE'] = env.get('VSWHERE', msvc_find_vswhere()) 
-
     # Set-up ms tools paths
     msvc_setup_env_once(env, tool=tool_name)
 
diff --git a/SCons/Tool/msvc.xml b/SCons/Tool/msvc.xml
index 9f433a2453..1609bd7885 100644
--- a/SCons/Tool/msvc.xml
+++ b/SCons/Tool/msvc.xml
@@ -395,26 +395,30 @@ a warning is issued showing the versions actually discovered,
 and the build will eventually fail indicating a missing compiler binary.
 If &cv-MSVC_VERSION; is not set, &SCons; will (by default) select the
 latest version of &MSVC; installed on your system.
+</para>
+
+<para>
 The valid values for &cv-MSVC_VERSION; represent major versions
 of the compiler, except that versions ending in <literal>Exp</literal>
-refer to "Express" or "Express for Desktop" Visual Studio editions,
-which require distinct entries because they use a different
-filesystem layout and have feature limitations compared to
-the full version.
+refer to "Express" or "Express for Desktop" Visual Studio editions.
 Values that do not look like a valid compiler version
 <emphasis>string</emphasis> are not supported.
 </para>
 
 <para>
-To have the desired effect, &cv-MSVC_VERSION; must be set by the
-time compiler discovery takes place.
-If the default tools list
-or an explicit tools list including &t-link-msvc; is used,
-discovery takes place as the &consenv; is created,
-so passing it as an argument in the the &f-link-Environment; call
+To have the desired effect, &cv-MSVC_VERSION; must be set before an msvc &f-link-Tool;
+(e.g., &t-link-msvc;) or an msvc-dependent &f-link-Tool; (e.g., &t-link-midl;) is loaded
+into the &consenv;.
+</para>
+
+<para>
+If the default tools list or an explicit tools list is used that includes an
+msvc &f-link-Tool; (e.g., &t-link-msvc;) or an msvc-dependent &f-link-Tool; (e.g., &t-link-midl;);
+the &cv-MSVC_VERSION; is resolved when the &consenv; is created.
+In this case, passing &cv-MSVC_VERSION; as an argument in the &f-link-Environment; call
 is the effective solution.
-Otherwise, &cv-MSVC_VERSION; must be set before the first msvc tool is
-loaded into the environment.
+Otherwise, &cv-MSVC_VERSION; must be set before the first msvc &f-link-Tool; or 
+msvc-dependent &f-link-Tool; is loaded into the environment.
 See the manpage section "Construction Environments" for an example.
 </para>
 
@@ -423,12 +427,6 @@ The following table shows the correspondence
 of &cv-MSVC_VERSION; values to various version indicators
 ('x' is used as a placeholder for
 a single digit that can vary).
-Note that it is not necessary to install Visual Studio
-to build with &SCons; (for example, you can install only
-Build Tools), but if Visual Studio is installed,
-additional builders such as &b-link-MSVSSolution; and
-&b-link-MSVSProject; become available and will
-correspond to the indicated versions.
 </para>
 
 <informaltable>
@@ -478,10 +476,10 @@ Visual Studio </literallayout></entry>
       </row>
       <row>
         <entry> <literal>"14.1Exp"</literal> </entry>
-        <entry> 14.1 </entry>
-        <entry> 1910 </entry>
+        <entry> 14.1 or 14.1x </entry>
+        <entry> 191x </entry>
         <entry> Visual Studio 2017 Express </entry>
-        <entry> 15.0 </entry>
+        <entry> 15.x </entry>
       </row>
       <row>
         <entry> <literal>"14.0"</literal> </entry>
@@ -592,6 +590,31 @@ Visual Studio </literallayout></entry>
   </tgroup>
 </informaltable>
 
+<note>
+  <itemizedlist>
+
+    <listitem><para>
+    It is not necessary to install a Visual Studio IDE
+    to build with &SCons; (for example, you can install only
+    Build Tools), but when a Visual Studio IDE is installed,
+    additional builders such as &b-link-MSVSSolution; and
+    &b-link-MSVSProject; become available and correspond to
+    the specified versions.
+    </para></listitem>
+
+    <listitem><para>
+    Versions ending in <literal>Exp</literal> refer to historical
+    "Express" or "Express for Desktop" Visual Studio editions,
+    which had feature limitations compared to the full editions.
+    It is only necessary to specify the <literal>Exp</literal>
+    suffix to select the express edition when both express and 
+    non-express editions of the same product are installed
+    simulaneously. The <literal>Exp</literal> suffix is unnecessary,
+    but accepted, when only the express edition is installed.
+    </para></listitem>
+  </itemizedlist>
+</note>
+
 <para>
 The compilation environment can be further or more precisely specified through the
 use of several other &consvars;: see the descriptions of
@@ -842,34 +865,42 @@ Specify the location of <filename>vswhere.exe</filename>.
 
 <para>
     The <filename>vswhere.exe</filename> executable is distributed with Microsoft Visual Studio and Build
-    Tools since the 2017 edition, but is also available standalone.
+    Tools since the 2017 edition, but is also available as a standalone installation.
     It provides full information about installations of 2017 and later editions.
     With the <option>-legacy</option> argument, <filename>vswhere.exe</filename> can detect installations of the 2010 through 2015
     editions with limited data returned.
-If <envar>VSWHERE</envar> is set, &SCons; will use that location.
 </para>
 <para>
-    Otherwise &SCons; will look in the following locations and set <envar>VSWHERE</envar> to the path of the first <filename>vswhere.exe</filename>
-located.
+    If &cv-VSWHERE; is set to a <filename>vswhere.exe</filename> location, &SCons; will use that location.
+    When &cv-VSWHERE; is undefined, &SCons; will look in the following locations and set &cv-VSWHERE; to the path
+    of the first <filename>vswhere.exe</filename> located:
 </para>
 
 <itemizedlist>
 <listitem><para><literal>%ProgramFiles(x86)%\Microsoft Visual Studio\Installer</literal></para></listitem>
 <listitem><para><literal>%ProgramFiles%\Microsoft Visual Studio\Installer</literal></para></listitem>
 <listitem><para><literal>%ChocolateyInstall%\bin</literal></para></listitem>
+<listitem><para><literal>%LOCALAPPDATA%\Microsoft\WinGet\Links</literal></para></listitem>
+<listitem><para><literal>~\scoop\shims</literal></para></listitem>
+<listitem><para><literal>%SCOOP%\shims</literal></para></listitem>
 </itemizedlist>
 
 <para>
-  Note that <envar>VSWHERE</envar> must be set at the same time or prior to any of &t-link-msvc;, &t-link-msvs; , and/or &t-link-mslink; &f-link-Tool; being initialized.
+  Note that &cv-VSWHERE; must be set prior to the initial &MSVC; compiler discovery.
+  For example, &cv-VSWHERE; must be set at the same time or before the first msvc &f-link-Tool;
+  (e.g., &t-link-msvc;) or msvc-dependent &f-link-Tool; (e.g., &t-link-midl;) is initialized.
+</para>
 
-  Either set it as follows
+<para>
+  Either set it as follows:
 <programlisting>
 env = Environment(VSWHERE='c:/my/path/to/vswhere')
 </programlisting>
 
-or if your &consenv; is created specifying an empty tools list
-(or a list of tools which omits all of default, msvs, msvc, and mslink),
-and also before &f-link-env-Tool; is called to ininitialize any of those tools:
+Or, if your &consenv; is created specifying: (a) an empty tools list, or (b)
+a list of tools which omits all of default, msvc (e.g., &t-link-msvc;), and
+msvc-dependent tools (e.g., &t-link-midl;); and before &f-link-env-Tool;
+is called to initialize any of those tools:
 
 <programlisting>
     env = Environment(tools=[])
diff --git a/SCons/Tool/msvsTests.py b/SCons/Tool/msvsTests.py
index dd708d0a7e..7893cafdcc 100644
--- a/SCons/Tool/msvsTests.py
+++ b/SCons/Tool/msvsTests.py
@@ -420,6 +420,11 @@ def get(self, name, value=None):
     def Dir(self, name):
         return self.fs.Dir(name)
 
+    def subst(self, key):
+        if key[0] == '$':
+            key = key[1:]
+        return self[key]
+
 
 class RegKey:
     """key class for storing an 'open' registry key"""
@@ -579,7 +584,7 @@ def DummyQueryValue(key, value):
 def DummyExists(path) -> bool:
     return True
 
-def DummyVsWhere(msvc_version, env):
+def DummyVsWhere(msvc_version, vswhere_exe):
     # not testing versions with vswhere, so return none
     return None
 
@@ -635,9 +640,9 @@ def _TODO_test_merge_default_version(self) -> None:
         """Test the merge_default_version() function"""
         pass
 
-    def test_query_versions(self) -> None:
+    def test_query_versions(self, env=None) -> None:
         """Test retrieval of the list of visual studio versions"""
-        v1 = query_versions()
+        v1 = query_versions(env)
         assert not v1 or str(v1[0]) == self.highest_version, \
                (v1, self.highest_version)
         assert len(v1) == self.number_of_versions, v1
@@ -880,9 +885,9 @@ class msvs71TestCase(msvsTestCase):
 class msvs8ExpTestCase(msvsTestCase): # XXX: only one still not working
     """Test MSVS 8 Express Registry"""
     registry = DummyRegistry(regdata_8exp + regdata_cv)
-    default_version = '8.0Exp'
-    highest_version = '8.0Exp'
-    number_of_versions = 1
+    default_version = '8.0'
+    highest_version = '8.0'
+    number_of_versions = 2
     install_locs = {
         '6.0' : {},
         '7.0' : {},
@@ -947,7 +952,7 @@ class msvsEmptyTestCase(msvsTestCase):
     SCons.Util.RegEnumKey      = DummyEnumKey
     SCons.Util.RegEnumValue    = DummyEnumValue
     SCons.Util.RegQueryValueEx = DummyQueryValue
-    SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere
+    SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere
 
     os.path.exists = DummyExists # make sure all files exist :-)
     os.path.isfile = DummyExists # make sure all files are files :-)


Test patch:
diff --git a/test/MSVC/MSVC_SDK_VERSION.py b/test/MSVC/MSVC_SDK_VERSION.py
index e5eae6762c..0f0e25a65b 100644
--- a/test/MSVC/MSVC_SDK_VERSION.py
+++ b/test/MSVC/MSVC_SDK_VERSION.py
@@ -31,6 +31,10 @@
 from SCons.Tool.MSCommon.vc import get_installed_vcs_components
 from SCons.Tool.MSCommon import msvc_sdk_versions
 from SCons.Tool.MSCommon import msvc_toolset_versions
+from SCons.Tool.MSCommon.MSVC.Kind import (
+    msvc_version_is_express,
+    msvc_version_is_btdispatch,
+)
 import TestSCons
 
 test = TestSCons.TestSCons()
@@ -38,12 +42,28 @@
 
 installed_versions = get_installed_vcs_components()
 default_version = installed_versions[0]
-GE_VS2015_versions = [v for v in installed_versions if v.msvc_vernum >= 14.0]
-LT_VS2015_versions = [v for v in installed_versions if v.msvc_vernum < 14.0]
+
+GE_VS2015_supported_versions = []
+GE_VS2015_unsupported_versions = []
+LT_VS2015_unsupported_versions = []
+
+for v in installed_versions:
+    if v.msvc_vernum > 14.0:
+        GE_VS2015_supported_versions.append(v)
+    elif v.msvc_verstr == '14.0':
+        if msvc_version_is_express(v.msvc_version):
+            GE_VS2015_unsupported_versions.append((v, 'Express'))
+        elif msvc_version_is_btdispatch(v.msvc_version):
+            GE_VS2015_unsupported_versions.append((v, 'BTDispatch'))
+        else:
+            GE_VS2015_supported_versions.append(v)
+    else:
+        LT_VS2015_unsupported_versions.append(v)
+
 default_sdk_versions_uwp = msvc_sdk_versions(version=None, msvc_uwp_app=True)
 default_sdk_versions_def = msvc_sdk_versions(version=None, msvc_uwp_app=False)
 
-have_140 = any(v.msvc_verstr == '14.0' for v in GE_VS2015_versions)
+have_140 = any(v.msvc_verstr == '14.0' for v in installed_versions)
 
 def version_major(version):
     components = version.split('.')
@@ -64,9 +84,10 @@ def version_major_list(version_list):
         seen_major.add(major)
     return versions
 
-if GE_VS2015_versions:
+if GE_VS2015_supported_versions:
 
-    for supported in GE_VS2015_versions:
+    for supported in GE_VS2015_supported_versions:
+        # VS2017+ and VS2015 ('14.0')
 
         sdk_versions_uwp = msvc_sdk_versions(version=supported.msvc_version, msvc_uwp_app=True)
         sdk_versions_def = msvc_sdk_versions(version=supported.msvc_version, msvc_uwp_app=False)
@@ -176,8 +197,8 @@ def version_major_list(version_list):
                         """.format(repr(supported.msvc_version), repr(toolset_version))
                     ))
                     test.run(arguments='-Q -s', status=2, stderr=None)
-                    expect = "MSVCArgumentError: MSVC_SDK_VERSION ('8.1') and platform type ('UWP') constraint violation: toolset version {} > '14.0' VS2015:".format(
-                        repr(toolset_version)
+                    expect = "MSVCArgumentError: MSVC_SDK_VERSION ('8.1') and platform type ('UWP') constraint violation: toolset {} MSVC_VERSION {} > '14.0' VS2015:".format(
+                        repr(toolset_version), repr(supported.msvc_version)
                     )
                     test.must_contain_all(test.stderr(), expect)
 
@@ -203,9 +224,37 @@ def version_major_list(version_list):
                 ))
                 test.run(arguments='-Q -s', stdout='')
 
-if LT_VS2015_versions:
+if GE_VS2015_unsupported_versions:
+
+    for unsupported, kind_str in GE_VS2015_unsupported_versions:
+        # VS2015 Express
+
+        sdk_version = default_sdk_versions_def[0] if default_sdk_versions_def else '8.1'
+
+        test.write('SConstruct', textwrap.dedent(
+            """
+            DefaultEnvironment(tools=[])
+            env = Environment(MSVC_VERSION={}, MSVC_SDK_VERSION={}, tools=['msvc'])
+            """.format(repr(unsupported.msvc_version), repr(sdk_version))
+        ))
+        test.run(arguments='-Q -s', status=2, stderr=None)
+        expect = "MSVCArgumentError: MSVC_SDK_VERSION ({}) is not supported for MSVC_VERSION {} ({}):".format(
+            repr(sdk_version), repr(unsupported.msvc_version), repr(kind_str)
+        )
+        test.must_contain_all(test.stderr(), expect)
+
+        # MSVC_SCRIPT_ARGS sdk_version is not validated
+        test.write('SConstruct', textwrap.dedent(
+            """
+            DefaultEnvironment(tools=[])
+            env = Environment(MSVC_VERSION={}, MSVC_SCRIPT_ARGS={}, tools=['msvc'])
+            """.format(repr(unsupported.msvc_version), repr(sdk_version))
+        ))
+        test.run(arguments='-Q -s', stdout='')
+
+if LT_VS2015_unsupported_versions:
 
-    for unsupported in LT_VS2015_versions:
+    for unsupported in LT_VS2015_unsupported_versions:
         # must be VS2015 or later
 
         sdk_version = default_sdk_versions_def[0] if default_sdk_versions_def else '8.1'
diff --git a/test/MSVC/MSVC_TOOLSET_VERSION.py b/test/MSVC/MSVC_TOOLSET_VERSION.py
index a20cf8a319..e43f4d7f80 100644
--- a/test/MSVC/MSVC_TOOLSET_VERSION.py
+++ b/test/MSVC/MSVC_TOOLSET_VERSION.py
@@ -28,6 +28,7 @@
 """
 import textwrap
 
+import SCons.Tool.MSCommon.MSVC.Config as Config
 from SCons.Tool.MSCommon.vc import get_installed_vcs_components
 from SCons.Tool.MSCommon import msvc_toolset_versions
 
@@ -38,10 +39,31 @@
 
 installed_versions = get_installed_vcs_components()
 default_version = installed_versions[0]
+
 GE_VS2017_versions = [v for v in installed_versions if v.msvc_vernum >= 14.1]
 LT_VS2017_versions = [v for v in installed_versions if v.msvc_vernum < 14.1]
 LT_VS2015_versions = [v for v in LT_VS2017_versions if v.msvc_vernum < 14.0]
 
+known_buildseries = set(Config.MSVC_BUILDSERIES_EXTERNAL.keys())
+
+def get_toolset_buildseries_version(toolset_version):
+    comps = toolset_version.split('.')
+    buildseries_version = comps[0] + '.' + comps[1][0]
+    vc_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL[buildseries_version]
+    vc_version = vc_buildseries_def.vc_version
+    return vc_version
+
+def get_latest_buildseries_version(v):
+    vs_def = Config.MSVC_VERSION_EXTERNAL[v.msvc_verstr]
+    vc_version = vs_def.vc_buildtools_def.vc_buildseries_list[0].vc_version
+    return vc_version
+
+def is_buildseries_known(toolset_version):
+    comps = toolset_version.split('.')
+    buildseries_version = comps[0] + '.' + comps[1][0]
+    rval = bool(buildseries_version in known_buildseries)
+    return rval
+
 if GE_VS2017_versions:
     # VS2017 and later for toolset argument
 
@@ -53,6 +75,11 @@
         toolset_sxs_versions = msvc_toolset_versions(supported.msvc_version, full=False, sxs=True)
         toolset_sxs_version = toolset_sxs_versions[0] if toolset_sxs_versions else None
 
+        toolset_version = toolset_full_version if toolset_full_version else supported.msvc_version
+        buildseries_version = get_toolset_buildseries_version(toolset_version)
+
+        latest_buildseries_version = get_latest_buildseries_version(supported)
+
         if toolset_full_version:
 
             # toolset version using construction variable
@@ -90,21 +117,21 @@
             ))
             test.run(arguments='-Q -s', stdout='')
 
-        # msvc_version as toolset version
+        # build series version as toolset version
         test.write('SConstruct', textwrap.dedent(
             """
             DefaultEnvironment(tools=[])
             env = Environment(MSVC_VERSION={}, MSVC_TOOLSET_VERSION={}, tools=['msvc'])
-            """.format(repr(supported.msvc_version), repr(supported.msvc_verstr))
+            """.format(repr(supported.msvc_version), repr(buildseries_version))
         ))
         test.run(arguments='-Q -s', stdout='')
 
-        # msvc_version as toolset version using script argument
+        # build series version as toolset version using script argument
         test.write('SConstruct', textwrap.dedent(
             """
             DefaultEnvironment(tools=[])
             env = Environment(MSVC_VERSION={}, MSVC_SCRIPT_ARGS='-vcvars_ver={}', tools=['msvc'])
-            """.format(repr(supported.msvc_version), supported.msvc_verstr)
+            """.format(repr(supported.msvc_version), buildseries_version)
         ))
         test.run(arguments='-Q -s', stdout='')
 
@@ -113,11 +140,11 @@
             """
             DefaultEnvironment(tools=[])
             env = Environment(MSVC_VERSION={}, MSVC_TOOLSET_VERSION={}, MSVC_SCRIPT_ARGS='-vcvars_ver={}', tools=['msvc'])
-            """.format(repr(supported.msvc_version), repr(supported.msvc_verstr), supported.msvc_verstr)
+            """.format(repr(supported.msvc_version), repr(buildseries_version), buildseries_version)
         ))
         test.run(arguments='-Q -s', status=2, stderr=None)
         expect = "MSVCArgumentError: multiple toolset version declarations: MSVC_TOOLSET_VERSION={} and MSVC_SCRIPT_ARGS='-vcvars_ver={}':".format(
-            repr(supported.msvc_verstr), supported.msvc_verstr
+            repr(buildseries_version), buildseries_version
         )
         test.must_contain_all(test.stderr(), expect)
 
@@ -149,9 +176,19 @@
         )
         test.must_contain_all(test.stderr(), expect)
 
-        # msvc_toolset_version is invalid (version greater than msvc version)
-        invalid_toolset_vernum = round(supported.msvc_vernum + 0.1, 1)
+        # msvc_toolset_version is invalid (version not supported for msvc version)
+        invalid_toolset_vernum = round(float(latest_buildseries_version) + 0.1, 1)
         invalid_toolset_version = str(invalid_toolset_vernum)
+
+        if is_buildseries_known(invalid_toolset_version):
+            expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} > {} MSVC_VERSION:".format(
+                repr(invalid_toolset_version), repr(invalid_toolset_version), repr(supported.msvc_version)
+            )
+        else:
+            expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION {} build series {} is not supported:".format(
+                repr(invalid_toolset_version), repr(invalid_toolset_version)
+            )
+
         test.write('SConstruct', textwrap.dedent(
             """
             DefaultEnvironment(tools=[])
@@ -159,9 +196,6 @@
             """.format(repr(supported.msvc_version), repr(invalid_toolset_version))
         ))
         test.run(arguments='-Q -s', status=2, stderr=None)
-        expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} > {} MSVC_VERSION:".format(
-            repr(invalid_toolset_version), repr(invalid_toolset_version), repr(supported.msvc_version)
-        )
         test.must_contain_all(test.stderr(), expect)
 
         # msvc_toolset_version is invalid (version less than 14.0)
@@ -173,7 +207,7 @@
             """.format(repr(supported.msvc_version), repr(invalid_toolset_version))
         ))
         test.run(arguments='-Q -s', status=2, stderr=None)
-        expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} < '14.0' VS2015:".format(
+        expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} < '14.0' VS2015:".format(
             repr(invalid_toolset_version), repr(invalid_toolset_version)
         )
         test.must_contain_all(test.stderr(), expect)
@@ -187,7 +221,7 @@
             """.format(repr(supported.msvc_version), repr(invalid_toolset_version))
         ))
         test.run(arguments='-Q -s', status=2, stderr=None)
-        expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} != '14.0':".format(
+        expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} != '14.0':".format(
             repr(invalid_toolset_version), repr(invalid_toolset_version)
         )
         test.must_contain_all(test.stderr(), expect)
diff --git a/test/MSVC/MSVC_UWP_APP.py b/test/MSVC/MSVC_UWP_APP.py
index 30b07ef771..fd52fd8f5e 100644
--- a/test/MSVC/MSVC_UWP_APP.py
+++ b/test/MSVC/MSVC_UWP_APP.py
@@ -30,14 +30,33 @@
 import re
 
 from SCons.Tool.MSCommon.vc import get_installed_vcs_components
+from SCons.Tool.MSCommon.vc import get_native_host_platform
+from SCons.Tool.MSCommon.vc import _GE2022_HOST_TARGET_CFG
+from SCons.Tool.MSCommon.MSVC.Kind import (
+    msvc_version_is_express,
+    msvc_version_is_btdispatch,
+)
 import TestSCons
 
 test = TestSCons.TestSCons()
 test.skip_if_not_msvc()
 
 installed_versions = get_installed_vcs_components()
-GE_VS2015_versions = [v for v in installed_versions if v.msvc_vernum >= 14.0]
-LT_VS2015_versions = [v for v in installed_versions if v.msvc_vernum < 14.0]
+
+GE_VS2015_supported_versions = []
+GE_VS2015_unsupported_versions = []
+LT_VS2015_unsupported_versions = []
+
+for v in installed_versions:
+    if v.msvc_vernum > 14.0:
+        GE_VS2015_supported_versions.append(v)
+    elif v.msvc_verstr == '14.0':
+        if msvc_version_is_btdispatch(v.msvc_version):
+            GE_VS2015_unsupported_versions.append((v, 'BTDispatch'))
+        else:
+            GE_VS2015_supported_versions.append(v)
+    else:
+        LT_VS2015_unsupported_versions.append(v)
 
 # Look for the Store VC Lib paths in the LIBPATH:
 # [VS install path]\VC\LIB\store[\arch] and
@@ -46,32 +65,52 @@
 # C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\LIB\store\amd64
 # C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\LIB\store\references
 
+# By default, 2015 Express supports the store argument only for x86 targets.
+# Using MSVC_SCRIPT_ARGS to set the store argument is not validated and
+# will result in the store paths not found on 64-bit hosts when using the
+# default target architecture. 
+
+# By default, 2015 BTDispatch silently ignores the store argument.
+# Using MSVC_SCRIPT_ARGS to set the store argument is not validated and
+# will result in the store paths not found. 
+
+re_lib_eq2015exp_1 = re.compile(r'\\vc\\lib\\store', re.IGNORECASE)
+
 re_lib_eq2015_1 = re.compile(r'\\vc\\lib\\store\\references', re.IGNORECASE)
 re_lib_eq2015_2 = re.compile(r'\\vc\\lib\\store', re.IGNORECASE)
 
 re_lib_ge2017_1 = re.compile(r'\\lib\\x86\\store\\references', re.IGNORECASE)
 re_lib_ge2017_2 = re.compile(r'\\lib\\x64\\store', re.IGNORECASE)
 
-
 def check_libpath(msvc, active, output):
+
     def _check_libpath(msvc, output):
+        is_supported = True
         outdict = {key.strip(): val.strip() for key, val in [line.split('|') for line in output.splitlines()]}
         platform = outdict.get('PLATFORM', '')
         libpath = outdict.get('LIBPATH', '')
+        uwpsupported = outdict.get('UWPSUPPORTED', '')
+        if uwpsupported and uwpsupported.split('|')[-1] == '0':
+            is_supported = False
         n_matches = 0
         if msvc.msvc_verstr == '14.0':
+            if msvc_version_is_express(msvc.msvc_version):
+                for regex in (re_lib_eq2015exp_1,):
+                    if regex.search(libpath):
+                        n_matches += 1
+                return n_matches > 0, 'store', libpath, is_supported
             for regex in (re_lib_eq2015_1, re_lib_eq2015_2):
                 if regex.search(libpath):
                     n_matches += 1
-            return n_matches >= 2, 'store', libpath
+            return n_matches >= 2, 'store', libpath, is_supported
         elif platform == 'UWP':
             for regex in (re_lib_ge2017_1, re_lib_ge2017_2):
                 if regex.search(libpath):
                     n_matches += 1
-            return n_matches > 0, 'uwp', libpath
-        return False, 'uwp', libpath
+            return n_matches > 0, 'uwp', libpath, is_supported
+        return False, 'uwp', libpath, is_supported
 
-    found, kind, libpath = _check_libpath(msvc, output)
+    found, kind, libpath, is_supported = _check_libpath(msvc, output)
 
     failmsg = None
 
@@ -84,11 +123,13 @@ def _check_libpath(msvc, output):
             repr(msvc.msvc_version), repr(kind), repr(libpath)
         )
 
-    return failmsg
+    return failmsg, is_supported
 
-if GE_VS2015_versions:
+if GE_VS2015_supported_versions:
     # VS2015 and later for uwp/store argument
-    for supported in GE_VS2015_versions:
+
+    for supported in GE_VS2015_supported_versions:
+
         for msvc_uwp_app in (True, '1', False, '0', None):
             active = msvc_uwp_app in (True, '1')
 
@@ -102,7 +143,7 @@ def _check_libpath(msvc, output):
                 """.format(repr(supported.msvc_version), repr(msvc_uwp_app))
             ))
             test.run(arguments='-Q -s', stdout=None)
-            failmsg = check_libpath(supported, active, test.stdout())
+            failmsg, _ = check_libpath(supported, active, test.stdout())
             if failmsg:
                 test.fail_test(message=failmsg)
 
@@ -120,23 +161,77 @@ def _check_libpath(msvc, output):
             if not test.stderr().strip().startswith("MSVCArgumentError: multiple uwp declarations:"):
                 test.fail_test(message='Expected MSVCArgumentError')
 
-        # uwp using script argument
+        if supported.msvc_verstr == '14.0' and msvc_version_is_express(supported.msvc_version):
+
+            # uwp using script argument may not be supported for default target architecture
+            test.write('SConstruct', textwrap.dedent(
+                """
+                from SCons.Tool.MSCommon.MSVC.Kind import msvc_version_uwp_is_supported
+                DefaultEnvironment(tools=[])
+                env = Environment(MSVC_VERSION={}, MSVC_SCRIPT_ARGS='store', tools=['msvc'])
+                is_supported, _ = msvc_version_uwp_is_supported(env['MSVC_VERSION'], env['TARGET_ARCH'])
+                uwpsupported = '1' if is_supported else '0'
+                print('LIBPATH|' + env['ENV'].get('LIBPATH', ''))
+                print('PLATFORM|' + env['ENV'].get('VSCMD_ARG_app_plat',''))
+                print('UWPSUPPORTED|' + uwpsupported)
+                """.format(repr(supported.msvc_version))
+            ))
+            test.run(arguments='-Q -s', stdout=None)
+
+        else:
+
+            # uwp using script argument
+            test.write('SConstruct', textwrap.dedent(
+                """
+                DefaultEnvironment(tools=[])
+                env = Environment(MSVC_VERSION={}, MSVC_SCRIPT_ARGS='store', tools=['msvc'])
+                print('LIBPATH|' + env['ENV'].get('LIBPATH', ''))
+                print('PLATFORM|' + env['ENV'].get('VSCMD_ARG_app_plat',''))
+                """.format(repr(supported.msvc_version))
+            ))
+            test.run(arguments='-Q -s', stdout=None)
+
+        failmsg, is_supported = check_libpath(supported, True, test.stdout())
+        if failmsg and is_supported:
+            test.fail_test(message=failmsg)
+
+if GE_VS2015_unsupported_versions:
+    # VS2015 and later for uwp/store error
+
+    for unsupported, kind_str in GE_VS2015_unsupported_versions:
+
+        for msvc_uwp_app in (True, '1'):
+
+            # uwp using construction variable
+            test.write('SConstruct', textwrap.dedent(
+                """
+                DefaultEnvironment(tools=[])
+                env = Environment(MSVC_VERSION={}, MSVC_UWP_APP={}, tools=['msvc'])
+                """.format(repr(unsupported.msvc_version), repr(msvc_uwp_app))
+            ))
+            test.run(arguments='-Q -s', status=2, stderr=None)
+            expect = "MSVCArgumentError: MSVC_UWP_APP ({}) is not supported for MSVC_VERSION {} ({}):".format(
+                repr(msvc_uwp_app), repr(unsupported.msvc_version), repr(kind_str)
+            )
+            test.must_contain_all(test.stderr(), expect)
+
+        # MSVC_SCRIPT_ARGS store is not validated
         test.write('SConstruct', textwrap.dedent(
             """
             DefaultEnvironment(tools=[])
             env = Environment(MSVC_VERSION={}, MSVC_SCRIPT_ARGS='store', tools=['msvc'])
-            print('LIBPATH|' + env['ENV'].get('LIBPATH', ''))
-            print('PLATFORM|' + env['ENV'].get('VSCMD_ARG_app_plat',''))
-            """.format(repr(supported.msvc_version))
+            """.format(repr(unsupported.msvc_version))
         ))
-        test.run(arguments='-Q -s', stdout=None)
-        failmsg = check_libpath(supported, True, test.stdout())
-        if failmsg:
-            test.fail_test(message=failmsg)
+        test.run(arguments='-Q -s', stdout='')
+        failmsg, _ = check_libpath(unsupported, True, test.stdout())
+        if not failmsg:
+            test.fail_test(message='unexpected: store found in libpath')
 
-if LT_VS2015_versions:
+if LT_VS2015_unsupported_versions:
     # VS2013 and earlier for uwp/store error
-    for unsupported in LT_VS2015_versions:
+
+    for unsupported in LT_VS2015_unsupported_versions:
+
         for msvc_uwp_app in (True, '1', False, '0', None):
             active = msvc_uwp_app in (True, '1')
 
diff --git a/test/MSVC/VSWHERE-fixture/SConstruct b/test/MSVC/VSWHERE-fixture/SConstruct
index 74eea18c16..0346f802e0 100644
--- a/test/MSVC/VSWHERE-fixture/SConstruct
+++ b/test/MSVC/VSWHERE-fixture/SConstruct
@@ -5,12 +5,16 @@
 import os
 import os.path
 
-from SCons.Tool.MSCommon.vc import VSWHERE_PATHS
+import SCons.Tool.MSCommon as mscommon
+import SCons.Tool.MSCommon.vc as vc
 
 # Dump out expected paths
-for vw_path in VSWHERE_PATHS:
-    print("VSWHERE_PATH=%s" % vw_path)
-
+for exegroup in (
+    vc._VSWHERE_EXEGROUP_MSVS,
+    vc._VSWHERE_EXEGROUP_PKGMGR
+):
+    for vw_path in exegroup:
+        print("VSWHERE_PATH=%s" % vw_path)
 
 # Allow normal detection logic to find vswhere.exe
 DefaultEnvironment(tools=[])
@@ -21,6 +25,19 @@ print("VSWHERE-detect=%s" % env1['VSWHERE'])
 v_local = os.path.join(os.getcwd(), 'vswhere.exe')
 Execute(Copy(os.path.join(os.getcwd(), 'vswhere.exe'), env1['VSWHERE']))
 
+# Reset vswhere executable manager
+# A vswhere.exe not equal to the vswhere.exe for initial detection is an error
+vc._VSWhereExecutable.reset()
+
 # With VSWHERE set to copied vswhere.exe (see above), find vswhere.exe
 env = Environment(VSWHERE=v_local)
 print("VSWHERE-env=%s" % env['VSWHERE'])
+
+# Reset vswhere executable manager
+# A vswhere.exe not equal to the vswhere.exe for initial detection is an error
+vc._VSWhereExecutable.reset()
+
+# With VSWHERE set to copied vswhere.exe (see above), find vswhere.exe
+mscommon.vswhere_register_executable(v_local, priority='high')
+env = Environment()
+print("VSWHERE-util=%s" % env['VSWHERE'])
diff --git a/test/MSVC/VSWHERE.py b/test/MSVC/VSWHERE.py
index 8212415f37..b38ae5f2da 100644
--- a/test/MSVC/VSWHERE.py
+++ b/test/MSVC/VSWHERE.py
@@ -28,6 +28,7 @@
 Also test that vswhere.exe is found and sets VSWHERE to the correct values
 """
 import os.path
+import SCons.Tool.MSCommon
 import TestSCons
 
 _python_ = TestSCons._python_
@@ -36,6 +37,10 @@
 test.skip_if_not_msvc()
 test.verbose_set(1)
 
+_default_vc = SCons.Tool.MSCommon.vc.get_installed_vcs_components()[0]
+if _default_vc.msvc_vernum < 14.1:
+    test.skip_test("no installed msvc requires vswhere.exe; skipping test\n")
+
 test.dir_fixture('VSWHERE-fixture')
 
 test.run(arguments=".")
@@ -57,11 +62,14 @@
         detected_path = l.strip().split('=')[-1]
     elif 'VSWHERE-env' in l:
         env_path = l.strip().split('=')[-1]
+    elif 'VSWHERE-util' in l:
+        util_path = l.strip().split('=')[-1]
 
 # Debug code
 # print("VPP:%s" % default_locations)
 # print("V-D:%s" % detected_path)
 # print("V-E:%s" % env_path)
+# print("V-U:%s" % util_path)
 
 
 test.fail_test(
@@ -82,6 +90,12 @@
     message='VSWHERE not\n\t%s\n\t but\n\t%s' % (expected_env_path, env_path),
 )
 
+expected_util_path = os.path.join(test.workdir, 'vswhere.exe')
+test.fail_test(
+    util_path != expected_env_path,
+    message='VSWHERE not\n\t%s\n\t but\n\t%s' % (expected_util_path, util_path),
+)
+
 test.pass_test()
 
 # here for reference, unused
@@ -94,6 +108,7 @@
 VSWHERE-detect=C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe
 Copy("C:\Users\Bill\AppData\Local\Temp\testcmd.11256.1ae1_as5\vswhere.exe", "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe")
 VSWHERE-env=C:\Users\Bill\AppData\Local\Temp\testcmd.11256.1ae1_as5\vswhere.exe
+VSWHERE-util=C:\Users\Bill\AppData\Local\Temp\testcmd.11256.1ae1_as5\vswhere.exe
 scons: done reading SConscript files.
 scons: Building targets ...
 scons: `.' is up to date.
diff --git a/test/MSVC/msvc.py b/test/MSVC/msvc.py
index 6bc36759a2..e81ecea7e8 100644
--- a/test/MSVC/msvc.py
+++ b/test/MSVC/msvc.py
@@ -116,7 +116,7 @@
 
 # TODO: Reevaluate if having this part of the test makes sense any longer
 # using precompiled headers should be faster
-limit = slow*0.90
+limit = slow*1.00
 if fast >= limit:
     print("Using precompiled headers was not fast enough:")
     print("slow.obj:  %.3fs" % slow)
diff --git a/test/MSVC/msvc_cache_force_defaults.py b/test/MSVC/msvc_cache_force_defaults.py
index e0ed1c3543..ad67304d56 100644
--- a/test/MSVC/msvc_cache_force_defaults.py
+++ b/test/MSVC/msvc_cache_force_defaults.py
@@ -30,6 +30,10 @@
 import textwrap
 
 from SCons.Tool.MSCommon.vc import get_installed_vcs_components
+from SCons.Tool.MSCommon.MSVC.Kind import (
+    msvc_version_is_express,
+    msvc_version_is_btdispatch,
+)
 import TestSCons
 
 test = TestSCons.TestSCons()
@@ -68,8 +72,15 @@
 test.run(arguments="-Q -s", status=0, stdout=None)
 cache_arg = test.stdout().strip()
 if default_version.msvc_verstr == '14.0':
-    # VS2015: target_arch msvc_sdk_version
-    expect = r'^SCRIPT_ARGS: .* [0-9.]+$'
+    if msvc_version_is_express(default_version.msvc_version):
+        # VS2015 Express: target_arch
+        expect = r'^SCRIPT_ARGS: [a-zA-Z0-9_]+$'
+    elif msvc_version_is_btdispatch(default_version.msvc_version):
+        # VS2015 BTDispatch: target_arch
+        expect = r'^SCRIPT_ARGS: [a-zA-Z0-9_]+$'
+    else:
+        # VS2015: target_arch msvc_sdk_version
+        expect = r'^SCRIPT_ARGS: [a-zA-Z0-9_]+ [0-9.]+$'
 else:
     # VS2017+ msvc_sdk_version msvc_toolset_version
     expect = r'^SCRIPT_ARGS: [0-9.]+ -vcvars_ver=[0-9.]+$'
diff --git a/test/MSVC/no_msvc.py b/test/MSVC/no_msvc.py
index 35cce9258d..aa9e2b8e57 100644
--- a/test/MSVC/no_msvc.py
+++ b/test/MSVC/no_msvc.py
@@ -75,8 +75,7 @@ def exists(env):
 
 # test no msvc's and msvc_query_version_toolset() call
 test.file_fixture('no_msvc/no_msvcs_sconstruct_msvc_query_toolset_version.py', 'SConstruct')
-test.run(arguments='-Q -s')
-test.must_contain_all(test.stdout(), 'msvc_version=None, msvc_toolset_version=None')
+test.run(arguments='-Q -s', status=2, stderr=r"^.*MSVCToolsetVersionNotFound.+", match=TestSCons.match_re_dotall)
 
 test.pass_test()
 
diff --git a/test/MSVC/query_vcbat.py b/test/MSVC/query_vcbat.py
index 6e30706b89..28bbd02dc3 100644
--- a/test/MSVC/query_vcbat.py
+++ b/test/MSVC/query_vcbat.py
@@ -51,7 +51,7 @@
 #            print k, v
 #MergeMSVSBatFile(env, 9.0)
 #print(env['ENV']['PATH'])
-print(query_versions())
+print(query_versions(env=None))
 """)
 
 test.run(stderr=None)
diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct.py b/test/fixture/no_msvc/no_msvcs_sconstruct.py
index 1aed9ecc1c..8dba65acba 100644
--- a/test/fixture/no_msvc/no_msvcs_sconstruct.py
+++ b/test/fixture/no_msvc/no_msvcs_sconstruct.py
@@ -7,7 +7,7 @@
 
 DefaultEnvironment(tools=[])
 
-def DummyVsWhere(msvc_version, env):
+def DummyVsWhere(msvc_version, vswhere_exe):
     # not testing versions with vswhere, so return none
     return None
 
@@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env):
         (SCons.Util.HKEY_LOCAL_MACHINE, r'')
     ]
 
-SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere
+SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere
 env = SCons.Environment.Environment()
 print('MSVC_VERSION=' + str(env.get('MSVC_VERSION')))
diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_query_toolset_version.py b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_query_toolset_version.py
index fc5558b54a..9fe9440321 100644
--- a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_query_toolset_version.py
+++ b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_query_toolset_version.py
@@ -7,7 +7,7 @@
 
 DefaultEnvironment(tools=[])
 
-def DummyVsWhere(msvc_version, env):
+def DummyVsWhere(msvc_version, vswhere_exe):
     # not testing versions with vswhere, so return none
     return None
 
@@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env):
         (SCons.Util.HKEY_LOCAL_MACHINE, r'')
     ]
 
-SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere
+SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere
 msvc_version, msvc_toolset_version = SCons.Tool.MSCommon.msvc_query_version_toolset()
 print(f'msvc_version={msvc_version!r}, msvc_toolset_version={msvc_toolset_version!r}')
diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_sdk_versions.py b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_sdk_versions.py
index 6e7562d390..1880b8bf7a 100644
--- a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_sdk_versions.py
+++ b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_sdk_versions.py
@@ -7,7 +7,7 @@
 
 DefaultEnvironment(tools=[])
 
-def DummyVsWhere(msvc_version, env):
+def DummyVsWhere(msvc_version, vswhere_exe):
     # not testing versions with vswhere, so return none
     return None
 
@@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env):
         (SCons.Util.HKEY_LOCAL_MACHINE, r'')
     ]
 
-SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere
+SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere
 sdk_version_list = SCons.Tool.MSCommon.msvc_sdk_versions()
 print('sdk_version_list=' + repr(sdk_version_list))
diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_toolset_versions.py b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_toolset_versions.py
index c2208cd393..35a45c3b62 100644
--- a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_toolset_versions.py
+++ b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_toolset_versions.py
@@ -7,7 +7,7 @@
 
 DefaultEnvironment(tools=[])
 
-def DummyVsWhere(msvc_version, env):
+def DummyVsWhere(msvc_version, vswhere_exe):
     # not testing versions with vswhere, so return none
     return None
 
@@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env):
         (SCons.Util.HKEY_LOCAL_MACHINE, r'')
     ]
 
-SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere
+SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere
 toolset_version_list = SCons.Tool.MSCommon.msvc_toolset_versions()
 print('toolset_version_list=' + repr(toolset_version_list))
diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct_tools.py b/test/fixture/no_msvc/no_msvcs_sconstruct_tools.py
index 2f2f07aedc..baac01bb2c 100644
--- a/test/fixture/no_msvc/no_msvcs_sconstruct_tools.py
+++ b/test/fixture/no_msvc/no_msvcs_sconstruct_tools.py
@@ -7,7 +7,7 @@
 
 DefaultEnvironment(tools=[])
 
-def DummyVsWhere(msvc_version, env):
+def DummyVsWhere(msvc_version, vswhere_exe):
     # not testing versions with vswhere, so return none
     return None
 
@@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env):
         (SCons.Util.HKEY_LOCAL_MACHINE, r'')
     ]
 
-SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere
+SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere
 env = SCons.Environment.Environment(tools=['myignoredefaultmsvctool'])
 
diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct_version.py b/test/fixture/no_msvc/no_msvcs_sconstruct_version.py
index bd81688e35..3bf15d35e9 100644
--- a/test/fixture/no_msvc/no_msvcs_sconstruct_version.py
+++ b/test/fixture/no_msvc/no_msvcs_sconstruct_version.py
@@ -7,7 +7,7 @@
 
 DefaultEnvironment(tools=[])
 
-def DummyVsWhere(msvc_version, env):
+def DummyVsWhere(msvc_version, vswhere_exe):
     # not testing versions with vswhere, so return none
     return None
 
@@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env):
         (SCons.Util.HKEY_LOCAL_MACHINE, r'')
     ]
 
-SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere
+SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere
 SCons.Tool.MSCommon.msvc_set_notfound_policy('error')
 env = SCons.Environment.Environment(MSVC_VERSION='14.3')
diff --git a/testing/framework/TestSConsMSVS.py b/testing/framework/TestSConsMSVS.py
index 8c34372b85..91aa329d66 100644
--- a/testing/framework/TestSConsMSVS.py
+++ b/testing/framework/TestSConsMSVS.py
@@ -680,7 +680,7 @@ def msvs_versions(self):
 import SCons
 import SCons.Tool.MSCommon
 print("self.scons_version =%%s"%%repr(SCons.__%s__))
-print("self._msvs_versions =%%s"%%str(SCons.Tool.MSCommon.query_versions()))
+print("self._msvs_versions =%%s"%%str(SCons.Tool.MSCommon.query_versions(env=None)))
 """ % 'version'
 
             self.run(arguments = '-n -q -Q -f -', stdin = input)
@@ -738,13 +738,13 @@ def msvs_substitute(self, input, msvs_ver,
         result = result.replace('<SCC_SLN_INFO>\n', sln_sccinfo)
         return result
 
-    def get_msvs_executable(self, version):
+    def get_msvs_executable(self, version, env=None):
         """Returns a full path to the executable (MSDEV or devenv)
         for the specified version of Visual Studio.
         """
         from SCons.Tool.MSCommon import get_vs_by_version
 
-        msvs = get_vs_by_version(version)
+        msvs = get_vs_by_version(version, env)
         if not msvs:
             return None
         return msvs.get_executable()