Repository: SCons/scons (language: python)

Issue:
msvc initialization can blow up if nonexistant or not installed TARGET_ARCH requested
The scenario is:  on a lightly provisioned (but working) Windows instance with VS2022, `test/MSVC/TARGET_ARCH.py` blew up scons with an `IndexError` indexing into an empty list.  In fact, the third and fourth tests in that file both do this, the specific lines are these:
```py
env = Environment(tools=['default', 'msvc'], TARGET_ARCH = 'arm', MSVC_VERSION='11.0')

env = Environment(tools=['default', 'msvc'], TARGET_ARCH = 'arm64', MSVC_VERSION='11.0')
```

It takes a little work to generate a backtrace (I ended up pulling it into a standalone SConstruct and running outside the test framework), but the output including trace looks like this, trimmed down. I'll attach the full trace also, but I don't think the rest of it is interesting:
```py

scons: warning: unsupported host, target combination ('amd64', 'arm64') for MSVC version 11.0
File "C:\Users\mats\github\scons\TRY.THIS.4", line 1, in <module>

scons: warning: MSVC version '11.0' was not found.
  No versions of the MSVC compiler were found.

  Visual Studio C/C++ compilers may not be set correctly
File "C:\Users\mats\github\scons\TRY.THIS.4", line 1, in <module>
scons: *** [.] IndexError : list index out of range
scons: internal stack trace:
  File "C:\Users\mats\github\scons\scripts\..\SCons\Taskmaster\Job.py", line 219, in start
    task.prepare()
....
  File "C:\Users\mats\github\scons\scripts\..\SCons\Tool\mslink.py", line 312, in generate
    msvc_setup_env_once(env, tool=tool_name)
  File "C:\Users\mats\github\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 1113, in msvc_setup_env_once
    req_tools = MSVC.SetupEnvDefault.register_iserror(env, tool, msvc_exists)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\mats\github\scons\scripts\..\SCons\Tool\MSCommon\MSVC\SetupEnvDefault.py", line 191, in register_iserror
    re_nchar_min, re_tools_min = _Data.default_tools_re_list[-1]
                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^
```

Adding a little check shows in this circumstance `_Data.default_tools_re_list` is empty, seems like a guard against this case is needed.


[msvciniterr.txt](https://github.com/SCons/scons/files/10875065/msvciniterr.txt)

MSVC: Configure test failure on non-AMD64 hosts with default VS 2015+ target architecture
The generated  c source file for test ```conf.CheckFunc('printf')``` in ```test\Configure\config-h.py``` produces a linker error due to an unresolved ```printf``` symbol for non-```AMD64``` Windows hosts using the default target architecture for VS2015 and later.

The crux of the problem is using ```printf``` without an explicit ```#include <stdio.h>``` for VS2015 and later with a target architecture that is not ```AMD64```.

For VS2015 and later on non-```AMD64``` hosts and using the default target architecture, the ```config-h.py``` test fails due to ```printf```:
```
STDOUT =========================================================================
2c2
< Checking for C function printf()... yes
---
> Checking for C function printf()... no
```

It appears that starting with VS2015, ```printf``` is only exported from the ```AMD64``` libraries.  The MSVC ```<stdio.h>``` header generates inline functions that invoke a ```printf``` compatible function that is exported from the architecture specific libraries.  Without inclusion of ```<stdio.h>```, the test will fail for VS2015+ when the target architecture is not ```AMD64```.

Test environments:
* Windows 10 x86 (VMWare) Host with VS2019 BuildTools (default target architecture is ```'x86'```)
* Windows 11 ARM64 Host with VS2022 Community installed (WIP branch: default target architecture is ```'ARM64'```)

See below for additional details.
msvc initialization can blow up if nonexistant or not installed TARGET_ARCH requested
The scenario is:  on a lightly provisioned (but working) Windows instance with VS2022, `test/MSVC/TARGET_ARCH.py` blew up scons with an `IndexError` indexing into an empty list.  In fact, the third and fourth tests in that file both do this, the specific lines are these:
```py
env = Environment(tools=['default', 'msvc'], TARGET_ARCH = 'arm', MSVC_VERSION='11.0')

env = Environment(tools=['default', 'msvc'], TARGET_ARCH = 'arm64', MSVC_VERSION='11.0')
```

It takes a little work to generate a backtrace (I ended up pulling it into a standalone SConstruct and running outside the test framework), but the output including trace looks like this, trimmed down. I'll attach the full trace also, but I don't think the rest of it is interesting:
```py

scons: warning: unsupported host, target combination ('amd64', 'arm64') for MSVC version 11.0
File "C:\Users\mats\github\scons\TRY.THIS.4", line 1, in <module>

scons: warning: MSVC version '11.0' was not found.
  No versions of the MSVC compiler were found.

  Visual Studio C/C++ compilers may not be set correctly
File "C:\Users\mats\github\scons\TRY.THIS.4", line 1, in <module>
scons: *** [.] IndexError : list index out of range
scons: internal stack trace:
  File "C:\Users\mats\github\scons\scripts\..\SCons\Taskmaster\Job.py", line 219, in start
    task.prepare()
....
  File "C:\Users\mats\github\scons\scripts\..\SCons\Tool\mslink.py", line 312, in generate
    msvc_setup_env_once(env, tool=tool_name)
  File "C:\Users\mats\github\scons\scripts\..\SCons\Tool\MSCommon\vc.py", line 1113, in msvc_setup_env_once
    req_tools = MSVC.SetupEnvDefault.register_iserror(env, tool, msvc_exists)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\mats\github\scons\scripts\..\SCons\Tool\MSCommon\MSVC\SetupEnvDefault.py", line 191, in register_iserror
    re_nchar_min, re_tools_min = _Data.default_tools_re_list[-1]
                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^
```

Adding a little check shows in this circumstance `_Data.default_tools_re_list` is empty, seems like a guard against this case is needed.


[msvciniterr.txt](https://github.com/SCons/scons/files/10875065/msvciniterr.txt)



Discussion / hints:
@jcbrill I can make the test pass by just wrapping the block of code in lines 191-203 in a ` if _Data.default_tools_re_list:` but wanted to know if there was "more to it than this" in your opinion....
@mwichmann I'll look into it.

At first glance, I'm a little concerned about why it reports that no versions of the MSVC compiler were found:
```
scons: warning: unsupported host, target combination ('amd64', 'arm64') for MSVC version 11.0
File "C:\Users\mats\github\scons\TRY.THIS.4", line 1, in <module>

scons: warning: MSVC version '11.0' was not found.
  No versions of the MSVC compiler were found.
  Visual Studio C/C++ compilers may not be set correctly
```

@mwichmann If you have it running "stand-alone", is there any chance you could enable the MSCommon debugging (i.e., ```set SCONS_MSCOMMON_DEBUG=msvciniterr-debug.txt```) and post the log?
Was just typing that I hadn't captured the debug.... will do that
debug log added

[debug.txt](https://github.com/SCons/scons/files/10875764/debug.txt)

Meanwhile, it's certainly finding a compiler - 14.3 (I can add the cache file too if useful)... there was no arm or arm64 support in 11.0, but I assume we're not smart enough to know that much?
Does the  toolset ```14.35.32215``` folder exist and is it populated?
```C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx64\arm64\cl.exe```
No... in the directory a bit up, `Hostx64` and `Hostx86` only have `x64` and `x86` subdirectories, not `arm` or `amd64`.  Nor does my fully built-up system, although it does have arm64 dirs in the 2019 install...
Still not sure what is happening.  The log for 14.3 reports:
```
00397ms:MSCommon/vc.py:get_installed_vcs#945: trying to find VC 14.3
00397ms:MSCommon/vc.py:find_vc_pdir_vswhere#677: VSWHERE: C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe
00397ms:MSCommon/vc.py:find_vc_pdir_vswhere#682: running: ['C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe', '-version', '[17.0, 18.0)', '-property', 'installationPath']
00537ms:MSCommon/vc.py:find_vc_pdir#739: VC found: '14.3'
00537ms:MSCommon/vc.py:get_installed_vcs#949: found VC 14.3
00537ms:MSCommon/vc.py:get_host_target#473: HOST_ARCH:x86_64
00537ms:MSCommon/vc.py:get_host_target#481: TARGET_ARCH:arm64
00537ms:MSCommon/vc.py:_check_cl_exists_in_vc_dir#851: host_platform amd64, target_platform arm64 host_target_list [('amd64', 'arm64'), ('x86', 'arm64')]
00537ms:MSCommon/vc.py:_check_cl_exists_in_vc_dir#876: host platform amd64, target platform arm64 for version 14.3
00537ms:MSCommon/vc.py:_check_cl_exists_in_vc_dir#885: checking for cl.exe at C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx64\arm64\cl.exe
00537ms:MSCommon/vc.py:_check_cl_exists_in_vc_dir#876: host platform x86, target platform arm64 for version 14.3
00537ms:MSCommon/vc.py:_check_cl_exists_in_vc_dir#885: checking for cl.exe at C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx86\arm64\cl.exe
00537ms:MSCommon/vc.py:get_installed_vcs#953: no compiler found 14.3
```

It appears like it is only checking 14.3 with a given ```TARGET_ARCH='arm64'```.

I'm a little rusty evidently...

EDIT: changed 14.2 to 14.3.
@mwichmann The root problem is with the construction of  the installed VCS cache list (i.e., ```__INSTALLED_VCS_RUN```) in vc.py.

The installed VCS list is cached (i.e., computed once).  However, there are indirect dependencies on values from the environment.  Most notably, the TARGET_ARCH. 

In some of the tests, when the installed VCS cache list is constructed, the TARGET_ARCH (e.g., ```arm``` and ```arm64```) does not exist in VS2022 and therefore there are no detected VCS.

The quick fix below uses an undefined TARGET_ARCH during the initial construction and then restores the original value prior to exit.  This should be enough to accurately detect all installations with at least one usable HOST/TARGET combination.

~~I'm not convinced that a user specified VSWHERE via the environment is guaranteed to be used in light of the installed VCS cache.  The VSWHERE specification might only work during the initial construction.  I could be really wrong though.~~

This is a quick fix for the immediate problem:
https://github.com/jcbrill/scons/tree/jbrill-msvc-vcscache

The ```_UNDEFINED``` object was moved to the top of the file as it was now being used in code "above" the original declaration.

Still not exactly sure how/why the original symptom is manifested yet.  It seems to have disappeared with the quick fix.
When testing on my development box which happens to have MSVC 11.0 Express installed, the arm batch file appears to succeed.

However, the debug log for the batch file invocation indicates usage errors but no exception is raised because cl.exe is found on the path.  I believe it to be the correct cl.exe as well.

Unfortunately, when reviewing the relevant code, there is a bug involving the host platform and likely just express versions due to an error of omission of an additional check for existence.  I need to test that an additional check does not break anything that is currently working as expected.
@mwichmann  updated branch available at:
https://github.com/jcbrill/scons/tree/jbrill-msvc-vcscache

The implementation now passes the msvc/msvs tests (79 tests, 5 skipped) on my development box and in a VMWare virtual machine with a minimal VS2022 (x86 and amd64 for latest 14.3 toolset) as described above (79 tests, 15 skipped).

Changes:
- The issue with the installed VCS cache list has been resolved and tweaked from the code listing above to handle the test suite when env=None is passed.
- A test for the existence of cl.exe for the host/target combination was added to the batch file existence check for VS6 to VS2015 in find_batch_file.  *There may be issues with VS2017+.*  I know there is a looming problem with VS2022 on ARM64 when trying to configure the ARM64 native batch files.
- The sdk_batch_file query was moved into its own function and the sdk script invocation(s) are no longer interleaved with the vc script invocations.  The sdk script invocations are only evaluated if all vc script host/target combinations have been  exhausted without finding a valid vc script.  The existence of cl.exe and the batch file combined is a "stronger" guarantee than the batch file existence alone.

I don't know if it's relevant to the issue under investigation here (and sorry haven't caught up with the above few comments), but came across these two errors in a pylint report: 
```
SCons/Tool/MSCommon/MSVC/SetupEnvDefault.py:87:19: E1135: Value '_Data.msvc_tools' doesn't support membership test (unsupported-membership-test)
SCons/Tool/MSCommon/MSVC/SetupEnvDefault.py:100:58: E1135: Value '_Data.msvc_tools' doesn't support membership test (unsupported-membership-test)
```
> (and sorry haven't caught up with the above few comments),

No worries.

_Data.msvc_tools is initialized to None.   It is later assigned a set().  In between there is code in functions that tests for membership.

I'm guessing that might be why pylint is complaining.  I'll take at look at the call sequence.

I believe that the issue under investigation is the result of an internal state being constructed improperly (i.e., the installed vcs cache) which causes other issues.

This morning I added the requisite configuration data structures for ARM64  hosts.
Good news: sconstruct builds are working as expected.
Bad news: due to some code movement and renamed configuration data structures 4 tests in the test suite now fail.
> _Data.msvc_tools is initialized to None... I'm guessing that might be why pylint is complaining

Yes, it has a LOT of complaints on the SCons codebase where it can't be 100% sure that something that starts out with a sentinel `None` actually arrives at a particular point with no chance of still being `None`.  Most of those are classes, where SCons tries to defer creating an instance until it's needed, so there's a bunch of `instance of Foo has no Bar member` because it thinks it could arrive there as `None`.  Sigh.
> > (and sorry haven't caught up with the above few comments),
> 
> No worries.
> 
> _Data.msvc_tools is initialized to None. It is later assigned a set(). In between there is code in functions that tests for membership.
> 
> I'm guessing that might be why pylint is complaining. I'll take at look at the call sequence.
> 
> I believe that the issue under investigation is the result of an internal state being constructed improperly (i.e., the installed vcs cache) which causes other issues.
> 
> This morning I added the requisite configuration data structures for ARM64 hosts. Good news: sconstruct builds are working as expected. Bad news: due to some code movement and renamed configuration data structures 4 tests in the test suite now fail.

Ah. yeah, that's a pain when tests start failing, even when you made things better.  I did try running that test with no caching, FWIW, which didn't seem to have an impact.
According to git blame, the installed vcs cache was added 13 years ago.  Back then, there weren't too many options for targets.

The code used to construct the installed vcs list indirectly evaluates TARGET_ARCH.  This is **BAD** for a cached value as the value of ```TARGET_ARCH``` can certainly change.

For the tests that fail, the installed vcs list is using ```arm``` and ```arm64```.  Since they are not installed, it believes that VS2022 is not installed.  This may have downstream consequences.

I'm not sure why it is failing now unless there was some change that might have affected the order in which environments are initialized (i.e., default vs user).

For the initial construction ```TARGET_ARCH=None``` or not defined at all is best.  The current "fix" is to forcibly delete the TARGET_ARCH, construct the list, and then restore the original value.
Unrelated: the final check to see if ```cl.exe``` is in the environment path appears to consider the os.environ['PATH'] if ```cl.exe``` is not found in the environment.

This could not possibly be a good idea.  I think in vc.py is should be ```env.WhereIs('cl')```.  Any thoughts? 

SCons\Tool\MSCommon.py (function msvc_setup_env, lines 1280-1288):
```
   ...
    # final check to issue a warning if the compiler is not present
    if not find_program_path(env, 'cl'):
        debug("did not find %s", _CL_EXE_NAME)
        if CONFIG_CACHE:
            propose = "SCONS_CACHE_MSVC_CONFIG caching enabled, remove cache file {} if out of date.".format(CONFIG_CACHE)
        else:
            propose = "It may need to be installed separately with Visual Studio."
        warn_msg = "Could not find MSVC compiler 'cl'. {}".format(propose)
        SCons.Warnings.warn(SCons.Warnings.VisualCMissingWarning, warn_msg)
...
```

find_program_path fragment:
```
def find_program_path(env, key_program, default_paths=None, add_path=False) -> Optional[str]:
...
    # First search in the SCons path
    path = env.WhereIs(key_program)
    if path:
        return path

    # Then in the OS path
    path = SCons.Util.WhereIs(key_program)
    if path:
        if add_path:
            env.AppendENVPath('PATH', os.path.dirname(path))
        return path
...
```
SCons.Util.WhereIs fragment:
```
    def WhereIs(file, path=None, pathext=None, reject=None) -> Optional[str]:
        if path is None:
            try:
                path = os.environ['PATH']
            except KeyError:
                return None
...
```


>I can make the test pass by just wrapping the block of code in lines 191-203 in a  if _Data.default_tools_re_list: but wanted to know if there was "more to it than this" in your opinion....

@mwichmann It is certainly a GOOD THING to make sure the list has at least one entry.  I added the test on my end along with a debug message if the list is empty.

*It was an error of omission and hubris to assume the list would be populated upon first use.  I've given myself a reprimand and a time-out.*

Given that the code incorrectly thought there were no installed VCs, I still don't understand why this issue did not arise in the previous testing concerning no VCs installed.  With the detection corrected, the tests pass without the additional check. However, better with the additional test than without it.

Still stress testing the detection fix and the ARM64 host support...

Research details, notes, and comments.

**MSVC Libraries with ```printf``` or ```_printf``` exported**

* The libraries searched below should be comprehensive when no compiler or linker options are specified.
  * VC6-VS2013: ***all architectures***
  * VS2015-VS2022: ***```AMD64``` architectures only***
* ```dumpbin /LINKERMEMBER <library>``` (non-default installation paths):

    ```
    libcmt.txt
        _printf C:\Software\MSVS-1998-60-Pro\VC98\Lib\LIBCMT.LIB
        _printf C:\Software\MSVS-2002-70-Pro\Vc7\lib\libcmt.lib
        _printf C:\Software\MSVS-2003-71-Pro\Vc7\lib\libcmt.lib
        _printf C:\Software\MSVS-2005-80-Pro\VC\lib\libcmt.lib
         printf C:\Software\MSVS-2005-80-Pro\VC\lib\amd64\libcmt.lib
        _printf C:\Software\MSVS-2008-90-Pro\VC\lib\libcmt.lib
         printf C:\Software\MSVS-2008-90-Pro\VC\lib\amd64\libcmt.lib
         printf C:\Software\MSVS-2008-90-Pro\VC\lib\ia64\libcmt.lib
        _printf C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib\libcmt.lib
         printf C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib\amd64\libcmt.lib
         printf C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib\ia64\libcmt.lib
        _printf C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\lib\libcmt.lib
         printf C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\lib\amd64\libcmt.lib
         printf C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\lib\arm\libcmt.lib
        _printf C:\Software\MSVS-2013-120-Com\VC\lib\libcmt.lib
         printf C:\Software\MSVS-2013-120-Com\VC\lib\amd64\libcmt.lib
         printf C:\Software\MSVS-2013-120-Com\VC\lib\arm\libcmt.lib
         printf C:\Program Files (x86)\Microsoft SDKs\Windows\v5.0\Lib\IA64\libcmt.lib
         printf C:\Program Files\Microsoft Platform SDK for Windows Server 2003 R2\Lib\AMD64\libcmt.lib
         printf C:\Program Files\Microsoft Platform SDK for Windows Server 2003 R2\Lib\IA64\libcmt.lib
         printf C:\Program Files\Microsoft Platform SDK\Lib\AMD64\libcmt.lib
         printf C:\Program Files\Microsoft Platform SDK\Lib\IA64\libcmt.lib
        _printf C:\Program Files\Microsoft SDKs\Windows\v6.0\VC\LIB\libcmt.lib
         printf C:\Program Files\Microsoft SDKs\Windows\v6.0\VC\LIB\IA64\libcmt.lib
         printf C:\Program Files\Microsoft SDKs\Windows\v6.0\VC\LIB\x64\libcmt.lib
    oldnames.txt
    kernel32.txt
    runtimeobject.txt
    libvcruntime.txt
    libucrt.txt
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10150.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10586.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.14393.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.15063.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.17134.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.17763.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.20348.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22000.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\ucrt\x64\libucrt.lib
    uuid.txt
    ```

**Generated c source file for ```conf.CheckFunc('printf')```**

* The generated c source file is:
    ```
    #include <assert.h>

    #ifdef __cplusplus
    extern "C"
    #endif
    char printf(void);

    #if _MSC_VER && !__INTEL_COMPILER
        #pragma function(printf)
    #endif

    int main(void) {
        #if defined (__stub_printf) || defined (__stub___printf)
            #error "printf has a GNU stub, cannot check"
        #else
            printf();
        #endif
        return 0;
    }
   ```
   
   VS2015+ with a non-```AMD64``` default target architecture results in linker error:
   * ```error LNK2019: unresolved external symbol _printf referenced in function _main```
   
**Documentation: 2.1. Building Simple C / C++ Programs**

* Here's the famous "Hello, World!" program in C:
    ```
    int
    main()
    {
        printf("Hello, world!\n");
    }
    ```
    
* And here's how to build it using SCons. Save the code above into hello.c, and enter the following into a file named SConstruct:
    ```
    Program('hello.c')
    ```
    
    ***The example fails to build for VS2015 and later if the default target architecture is not ```'AMD64'```.***  For example, VS2019 on a 32-bit Windows host.
    
* Adding ```#include <stdio.h>``` will build with the default target architecture on non-```AMD64``` Windows hosts with VS2015 and later:
    ```
    #include <stdio.h>
    int
    main()
    {
        printf("Hello, world!\n");
    }
    ```
    
   The ```#include <stdio.h>``` causes the MSVC c preprocessor to generate inline functions for ```printf``` that dispatch to functions that *are* exported from the architecture-specific libraries.

@jcbrill I can make the test pass by just wrapping the block of code in lines 191-203 in a ` if _Data.default_tools_re_list:` but wanted to know if there was "more to it than this" in your opinion....
@mwichmann I'll look into it.

At first glance, I'm a little concerned about why it reports that no versions of the MSVC compiler were found:
```
scons: warning: unsupported host, target combination ('amd64', 'arm64') for MSVC version 11.0
File "C:\Users\mats\github\scons\TRY.THIS.4", line 1, in <module>

scons: warning: MSVC version '11.0' was not found.
  No versions of the MSVC compiler were found.
  Visual Studio C/C++ compilers may not be set correctly
```

@mwichmann If you have it running "stand-alone", is there any chance you could enable the MSCommon debugging (i.e., ```set SCONS_MSCOMMON_DEBUG=msvciniterr-debug.txt```) and post the log?
Was just typing that I hadn't captured the debug.... will do that
debug log added

[debug.txt](https://github.com/SCons/scons/files/10875764/debug.txt)

Meanwhile, it's certainly finding a compiler - 14.3 (I can add the cache file too if useful)... there was no arm or arm64 support in 11.0, but I assume we're not smart enough to know that much?
Does the  toolset ```14.35.32215``` folder exist and is it populated?
```C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx64\arm64\cl.exe```
No... in the directory a bit up, `Hostx64` and `Hostx86` only have `x64` and `x86` subdirectories, not `arm` or `amd64`.  Nor does my fully built-up system, although it does have arm64 dirs in the 2019 install...
Still not sure what is happening.  The log for 14.3 reports:
```
00397ms:MSCommon/vc.py:get_installed_vcs#945: trying to find VC 14.3
00397ms:MSCommon/vc.py:find_vc_pdir_vswhere#677: VSWHERE: C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe
00397ms:MSCommon/vc.py:find_vc_pdir_vswhere#682: running: ['C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe', '-version', '[17.0, 18.0)', '-property', 'installationPath']
00537ms:MSCommon/vc.py:find_vc_pdir#739: VC found: '14.3'
00537ms:MSCommon/vc.py:get_installed_vcs#949: found VC 14.3
00537ms:MSCommon/vc.py:get_host_target#473: HOST_ARCH:x86_64
00537ms:MSCommon/vc.py:get_host_target#481: TARGET_ARCH:arm64
00537ms:MSCommon/vc.py:_check_cl_exists_in_vc_dir#851: host_platform amd64, target_platform arm64 host_target_list [('amd64', 'arm64'), ('x86', 'arm64')]
00537ms:MSCommon/vc.py:_check_cl_exists_in_vc_dir#876: host platform amd64, target platform arm64 for version 14.3
00537ms:MSCommon/vc.py:_check_cl_exists_in_vc_dir#885: checking for cl.exe at C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx64\arm64\cl.exe
00537ms:MSCommon/vc.py:_check_cl_exists_in_vc_dir#876: host platform x86, target platform arm64 for version 14.3
00537ms:MSCommon/vc.py:_check_cl_exists_in_vc_dir#885: checking for cl.exe at C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx86\arm64\cl.exe
00537ms:MSCommon/vc.py:get_installed_vcs#953: no compiler found 14.3
```

It appears like it is only checking 14.3 with a given ```TARGET_ARCH='arm64'```.

I'm a little rusty evidently...

EDIT: changed 14.2 to 14.3.
@mwichmann The root problem is with the construction of  the installed VCS cache list (i.e., ```__INSTALLED_VCS_RUN```) in vc.py.

The installed VCS list is cached (i.e., computed once).  However, there are indirect dependencies on values from the environment.  Most notably, the TARGET_ARCH. 

In some of the tests, when the installed VCS cache list is constructed, the TARGET_ARCH (e.g., ```arm``` and ```arm64```) does not exist in VS2022 and therefore there are no detected VCS.

The quick fix below uses an undefined TARGET_ARCH during the initial construction and then restores the original value prior to exit.  This should be enough to accurately detect all installations with at least one usable HOST/TARGET combination.

~~I'm not convinced that a user specified VSWHERE via the environment is guaranteed to be used in light of the installed VCS cache.  The VSWHERE specification might only work during the initial construction.  I could be really wrong though.~~

This is a quick fix for the immediate problem:
https://github.com/jcbrill/scons/tree/jbrill-msvc-vcscache

The ```_UNDEFINED``` object was moved to the top of the file as it was now being used in code "above" the original declaration.

Still not exactly sure how/why the original symptom is manifested yet.  It seems to have disappeared with the quick fix.
When testing on my development box which happens to have MSVC 11.0 Express installed, the arm batch file appears to succeed.

However, the debug log for the batch file invocation indicates usage errors but no exception is raised because cl.exe is found on the path.  I believe it to be the correct cl.exe as well.

Unfortunately, when reviewing the relevant code, there is a bug involving the host platform and likely just express versions due to an error of omission of an additional check for existence.  I need to test that an additional check does not break anything that is currently working as expected.
@mwichmann  updated branch available at:
https://github.com/jcbrill/scons/tree/jbrill-msvc-vcscache

The implementation now passes the msvc/msvs tests (79 tests, 5 skipped) on my development box and in a VMWare virtual machine with a minimal VS2022 (x86 and amd64 for latest 14.3 toolset) as described above (79 tests, 15 skipped).

Changes:
- The issue with the installed VCS cache list has been resolved and tweaked from the code listing above to handle the test suite when env=None is passed.
- A test for the existence of cl.exe for the host/target combination was added to the batch file existence check for VS6 to VS2015 in find_batch_file.  *There may be issues with VS2017+.*  I know there is a looming problem with VS2022 on ARM64 when trying to configure the ARM64 native batch files.
- The sdk_batch_file query was moved into its own function and the sdk script invocation(s) are no longer interleaved with the vc script invocations.  The sdk script invocations are only evaluated if all vc script host/target combinations have been  exhausted without finding a valid vc script.  The existence of cl.exe and the batch file combined is a "stronger" guarantee than the batch file existence alone.

I don't know if it's relevant to the issue under investigation here (and sorry haven't caught up with the above few comments), but came across these two errors in a pylint report: 
```
SCons/Tool/MSCommon/MSVC/SetupEnvDefault.py:87:19: E1135: Value '_Data.msvc_tools' doesn't support membership test (unsupported-membership-test)
SCons/Tool/MSCommon/MSVC/SetupEnvDefault.py:100:58: E1135: Value '_Data.msvc_tools' doesn't support membership test (unsupported-membership-test)
```
> (and sorry haven't caught up with the above few comments),

No worries.

_Data.msvc_tools is initialized to None.   It is later assigned a set().  In between there is code in functions that tests for membership.

I'm guessing that might be why pylint is complaining.  I'll take at look at the call sequence.

I believe that the issue under investigation is the result of an internal state being constructed improperly (i.e., the installed vcs cache) which causes other issues.

This morning I added the requisite configuration data structures for ARM64  hosts.
Good news: sconstruct builds are working as expected.
Bad news: due to some code movement and renamed configuration data structures 4 tests in the test suite now fail.
> _Data.msvc_tools is initialized to None... I'm guessing that might be why pylint is complaining

Yes, it has a LOT of complaints on the SCons codebase where it can't be 100% sure that something that starts out with a sentinel `None` actually arrives at a particular point with no chance of still being `None`.  Most of those are classes, where SCons tries to defer creating an instance until it's needed, so there's a bunch of `instance of Foo has no Bar member` because it thinks it could arrive there as `None`.  Sigh.
> > (and sorry haven't caught up with the above few comments),
> 
> No worries.
> 
> _Data.msvc_tools is initialized to None. It is later assigned a set(). In between there is code in functions that tests for membership.
> 
> I'm guessing that might be why pylint is complaining. I'll take at look at the call sequence.
> 
> I believe that the issue under investigation is the result of an internal state being constructed improperly (i.e., the installed vcs cache) which causes other issues.
> 
> This morning I added the requisite configuration data structures for ARM64 hosts. Good news: sconstruct builds are working as expected. Bad news: due to some code movement and renamed configuration data structures 4 tests in the test suite now fail.

Ah. yeah, that's a pain when tests start failing, even when you made things better.  I did try running that test with no caching, FWIW, which didn't seem to have an impact.
According to git blame, the installed vcs cache was added 13 years ago.  Back then, there weren't too many options for targets.

The code used to construct the installed vcs list indirectly evaluates TARGET_ARCH.  This is **BAD** for a cached value as the value of ```TARGET_ARCH``` can certainly change.

For the tests that fail, the installed vcs list is using ```arm``` and ```arm64```.  Since they are not installed, it believes that VS2022 is not installed.  This may have downstream consequences.

I'm not sure why it is failing now unless there was some change that might have affected the order in which environments are initialized (i.e., default vs user).

For the initial construction ```TARGET_ARCH=None``` or not defined at all is best.  The current "fix" is to forcibly delete the TARGET_ARCH, construct the list, and then restore the original value.
Unrelated: the final check to see if ```cl.exe``` is in the environment path appears to consider the os.environ['PATH'] if ```cl.exe``` is not found in the environment.

This could not possibly be a good idea.  I think in vc.py is should be ```env.WhereIs('cl')```.  Any thoughts? 

SCons\Tool\MSCommon.py (function msvc_setup_env, lines 1280-1288):
```
   ...
    # final check to issue a warning if the compiler is not present
    if not find_program_path(env, 'cl'):
        debug("did not find %s", _CL_EXE_NAME)
        if CONFIG_CACHE:
            propose = "SCONS_CACHE_MSVC_CONFIG caching enabled, remove cache file {} if out of date.".format(CONFIG_CACHE)
        else:
            propose = "It may need to be installed separately with Visual Studio."
        warn_msg = "Could not find MSVC compiler 'cl'. {}".format(propose)
        SCons.Warnings.warn(SCons.Warnings.VisualCMissingWarning, warn_msg)
...
```

find_program_path fragment:
```
def find_program_path(env, key_program, default_paths=None, add_path=False) -> Optional[str]:
...
    # First search in the SCons path
    path = env.WhereIs(key_program)
    if path:
        return path

    # Then in the OS path
    path = SCons.Util.WhereIs(key_program)
    if path:
        if add_path:
            env.AppendENVPath('PATH', os.path.dirname(path))
        return path
...
```
SCons.Util.WhereIs fragment:
```
    def WhereIs(file, path=None, pathext=None, reject=None) -> Optional[str]:
        if path is None:
            try:
                path = os.environ['PATH']
            except KeyError:
                return None
...
```


>I can make the test pass by just wrapping the block of code in lines 191-203 in a  if _Data.default_tools_re_list: but wanted to know if there was "more to it than this" in your opinion....

@mwichmann It is certainly a GOOD THING to make sure the list has at least one entry.  I added the test on my end along with a debug message if the list is empty.

*It was an error of omission and hubris to assume the list would be populated upon first use.  I've given myself a reprimand and a time-out.*

Given that the code incorrectly thought there were no installed VCs, I still don't understand why this issue did not arise in the previous testing concerning no VCs installed.  With the detection corrected, the tests pass without the additional check. However, better with the additional test than without it.

Still stress testing the detection fix and the ARM64 host support...


Fix patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 1c2a843bb8..3725fa473b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -16,6 +16,33 @@ RELEASE  VERSION/DATE TO BE FILLED IN LATER
     - Obsoleted YACCVCGFILESUFFIX, being replaced by YACC_GRAPH_FILE_SUFFIX.
       If YACC_GRAPH_FILE_SUFFIX is not set, it will respect YACCVCGFILESUFFIX.
 
+  From Joseph Brill:
+    - Fix issue #4312: the cached installed msvc list had an indirect dependency
+      on the target architecture in the environment dictionary.  The first call
+      to construct the installed msvc list now forces the target architecture to be
+      undefined, constructs the installed msvc list, and then restores the original
+      target architecture.
+      Note: an indirect dependency on the VSWHERE construction variable in the
+      environment remains.
+    - Fix issue #4312: explicitly guard against an empty regular expression list
+      when msvc is not installed.
+    - When trying to find a valid msvc batch file, check that the compiler executable
+      (cl.exe) exists for VS6 to VS2015 to avoid executing the msvc batch file.  Always
+      check that the compiler executable is found on the msvc script environment path
+      after running the msvc batch file.  Only use the sdk batch files when all of the
+      msvc script host/target combinations have been exhausted and a valid script was
+      not found.
+    - Add ARM64 host configurations for windows and msvc.
+      Note: VS2013 and earlier has not been tested on ARM64.
+    - If necessary, automatically define VSCMD_SKIP_SENDTELEMETRY for VS2019 and later
+      on ARM64 hosts when using an arm32 build of python to prevent a powershell dll
+      not found error pop-up window.
+    - Fix an issue where test SConfTests.py would fail when mscommon debugging
+      was enabled.  The mscommon debug filter class registered with the logging
+      module was refactored.
+    - Add arm64 to the MSVS supported architectures list for VS2017 and later to be
+      consistent with the current documentation of MSVS_ARCH.
+
   From Mats Wichmann
     - C scanner's dictifyCPPDEFINES routine did not understand the possible
       combinations of CPPDEFINES - not aware of a "name=value" string either
diff --git a/RELEASE.txt b/RELEASE.txt
index 024f049961..bffe74014a 100644
--- a/RELEASE.txt
+++ b/RELEASE.txt
@@ -19,6 +19,10 @@ NEW FUNCTIONALITY
 - D compilers : added support for generation of .di interface files.
   New variables DI_FILE_DIR, DI_FILE_DIR_PREFIX, DI_FILE_DIR_SUFFIX,
   DI_FILE_SUFFIX.
+- MSVC: If available, native arm64 tools will be used on arm64 hosts for VS2022.
+- MSVC: If necessary, automatically define VSCMD_SKIP_SENDTELEMETRY for VS2019 and later
+  on arm64 hosts when using an arm (32-bit) build of python to prevent a powershell
+  error pop-up window (powershell dll not found).
 
 DEPRECATED FUNCTIONALITY
 ------------------------
@@ -47,6 +51,14 @@ CHANGED/ENHANCED EXISTING FUNCTIONALITY
   Default value for $YACC_GRAPH_FILE_SUFFIX changed to '.gv' to match
   current bison default (since bison 3.8). Set this variable to '.dot'
   if using byacc. Fixes #4326 and #4327.
+- MSVC: When trying to find a valid msvc batch file, the existence of the msvc compiler
+  executable is verified for VS6 to VS2015 to avoid executing the msvc batch file when
+  the host/target tool is known not to be present. Always check that the msvc compiler
+  executable is found on the msvc script environment path after running the msvc batch
+  file.  This is predominately needed for recent versions of Visual Studio where the msvc
+  batch file exists but an individual msvc toolset may not support the host/target
+  architecture combination.  For example, when using VS2022 on arm64, the arm64 native
+  tools are only installed for the 14.3x toolsets.
 
 FIXES
 -----
@@ -59,6 +71,22 @@ FIXES
   embedded in a sequence, or by itself.  The conditional C scanner thus
   did not always properly apply the defines. The regular C scanner does
   not use these, so was not affected.  [fixes #4193]
+- MSVC: The installed msvc list is calculated once and cached.  There was an indirect
+  dependency on the target architecture when determining if each version of msvc
+  was installed based on the initial invocation.  It was possible that an msvc instance
+  would not be considered installed due to the tools for the requested target
+  architecture not being installed.  The initial call to construct the installed msvc
+  list now uses an undefined target architecture to evaluate all potential host/target
+  combinations when evaluating if the msvc tools are installed for a given Visual Studio
+  installation.
+- MSVC: Erroneous construction of the installed msvc list (as described above) caused an
+  index error in the msvc support code.  An explicit check was added to prevent indexing
+  into an empty list.  Fixes #4312.
+- MSCommon: Test SConfTests.py would fail when mscommon debugging was enabled via the
+  MSVC_MSCOMMON_DEBUG environment variable.  The mscommon logging filter class registered
+  with the python logging module was refactored to prevent test failure.
+- MSVS: Add arm64 to the MSVS supported architectures list for VS2017 and later to be
+  consistent with the current documentation of MSVS_ARCH.
 
 IMPROVEMENTS
 ------------
diff --git a/SCons/Platform/Platform.xml b/SCons/Platform/Platform.xml
index 08199f4c54..c449fa50da 100644
--- a/SCons/Platform/Platform.xml
+++ b/SCons/Platform/Platform.xml
@@ -173,8 +173,8 @@ else:
       Valid host arch values are
       <literal>x86</literal> and <literal>arm</literal>
       for 32-bit hosts and
-      <literal>amd64</literal> and <literal>x86_64</literal>
-      for 64-bit hosts.
+      <literal>amd64</literal>, <literal>arm64</literal>,
+      and <literal>x86_64</literal> for 64-bit hosts.
     </para>
     <para>
       Should be considered immutable. 
diff --git a/SCons/Platform/win32.py b/SCons/Platform/win32.py
index c9fcd9502d..275a5d4121 100644
--- a/SCons/Platform/win32.py
+++ b/SCons/Platform/win32.py
@@ -298,6 +298,11 @@ def __init__(self, arch, synonyms=[]) -> None:
         ['AMD64', 'amd64', 'em64t', 'EM64T', 'x86_64'],
     ),
 
+    ArchDefinition(
+        'arm64',
+        ['ARM64', 'aarch64', 'AARCH64', 'AArch64'],
+    ),
+
     ArchDefinition(
         'ia64',
         ['IA64'],
@@ -315,9 +320,20 @@ def get_architecture(arch=None):
     """Returns the definition for the specified architecture string.
 
     If no string is specified, the system default is returned (as defined
-    by the PROCESSOR_ARCHITEW6432 or PROCESSOR_ARCHITECTURE environment
-    variables).
+    by the registry PROCESSOR_ARCHITECTURE value, PROCESSOR_ARCHITEW6432
+    environment variable, PROCESSOR_ARCHITECTURE environment variable, or
+    the platform machine).
     """
+    if arch is None:
+        if SCons.Util.can_read_reg:
+            try:
+                k=SCons.Util.RegOpenKeyEx(SCons.Util.hkey_mod.HKEY_LOCAL_MACHINE,
+                                          'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment')
+                val, tok = SCons.Util.RegQueryValueEx(k, 'PROCESSOR_ARCHITECTURE')
+            except SCons.Util.RegError:
+                val = ''
+            if val and val in SupportedArchitectureMap:
+                arch = val
     if arch is None:
         arch = os.environ.get('PROCESSOR_ARCHITEW6432')
         if not arch:
diff --git a/SCons/Tool/MSCommon/MSVC/SetupEnvDefault.py b/SCons/Tool/MSCommon/MSVC/SetupEnvDefault.py
index 34e60ab19b..76b9ed654b 100644
--- a/SCons/Tool/MSCommon/MSVC/SetupEnvDefault.py
+++ b/SCons/Tool/MSCommon/MSVC/SetupEnvDefault.py
@@ -188,19 +188,22 @@ def register_iserror(env, tool, msvc_exists_func):
         debug('msvc default:check tools:nchar=%d, tools=%s', tools_nchar, tools)
 
         # iteratively remove default tool sequences (longest to shortest)
-        re_nchar_min, re_tools_min = _Data.default_tools_re_list[-1]
-        if tools_nchar >= re_nchar_min and re_tools_min.search(tools):
-            # minimum characters satisfied and minimum pattern exists
-            for re_nchar, re_default_tool in _Data.default_tools_re_list:
-                if tools_nchar < re_nchar:
-                    # not enough characters for pattern
-                    continue
-                tools = re_default_tool.sub('', tools).strip(_Data.separator)
-                tools_nchar = len(tools)
-                debug('msvc default:check tools:nchar=%d, tools=%s', tools_nchar, tools)
-                if tools_nchar < re_nchar_min or not re_tools_min.search(tools):
-                    # less than minimum characters or minimum pattern does not exist
-                    break
+        if not _Data.default_tools_re_list:
+            debug('default_tools_re_list=%s', _Data.default_tools_re_list)
+        else:
+            re_nchar_min, re_tools_min = _Data.default_tools_re_list[-1]
+            if tools_nchar >= re_nchar_min and re_tools_min.search(tools):
+                # minimum characters satisfied and minimum pattern exists
+                for re_nchar, re_default_tool in _Data.default_tools_re_list:
+                    if tools_nchar < re_nchar:
+                        # not enough characters for pattern
+                        continue
+                    tools = re_default_tool.sub('', tools).strip(_Data.separator)
+                    tools_nchar = len(tools)
+                    debug('msvc default:check tools:nchar=%d, tools=%s', tools_nchar, tools)
+                    if tools_nchar < re_nchar_min or not re_tools_min.search(tools):
+                        # less than minimum characters or minimum pattern does not exist
+                        break
 
         # construct non-default list(s) tools set
         tools_set = {msvc_tool for msvc_tool in tools.split(_Data.separator) if msvc_tool}
diff --git a/SCons/Tool/MSCommon/common.py b/SCons/Tool/MSCommon/common.py
index 0cadc10990..de460dc242 100644
--- a/SCons/Tool/MSCommon/common.py
+++ b/SCons/Tool/MSCommon/common.py
@@ -46,30 +46,31 @@ class MSVCCacheInvalidWarning(SCons.Warnings.WarningOnByDefault):
 if LOGFILE:
     import logging
 
-    modulelist = (
-        # root module and parent/root module
-        'MSCommon', 'Tool',
-        # python library and below: correct iff scons does not have a lib folder
-        'lib',
-        # scons modules
-        'SCons', 'test', 'scons'
-    )
+    class _Debug_Filter(logging.Filter):
+        # custom filter for module relative filename
 
-    def get_relative_filename(filename, module_list):
-        if not filename:
+        modulelist = (
+            # root module and parent/root module
+            'MSCommon', 'Tool',
+            # python library and below: correct iff scons does not have a lib folder
+            'lib',
+            # scons modules
+            'SCons', 'test', 'scons'
+        )
+
+        def get_relative_filename(self, filename, module_list):
+            if not filename:
+                return filename
+            for module in module_list:
+                try:
+                    ind = filename.rindex(module)
+                    return filename[ind:]
+                except ValueError:
+                    pass
             return filename
-        for module in module_list:
-            try:
-                ind = filename.rindex(module)
-                return filename[ind:]
-            except ValueError:
-                pass
-        return filename
 
-    class _Debug_Filter(logging.Filter):
-        # custom filter for module relative filename
         def filter(self, record) -> bool:
-            relfilename = get_relative_filename(record.pathname, modulelist)
+            relfilename = self.get_relative_filename(record.pathname, self.modulelist)
             relfilename = relfilename.replace('\\', '/')
             record.relfilename = relfilename
             return True
@@ -208,6 +209,17 @@ def has_reg(value) -> bool:
 # Functions for fetching environment variable settings from batch files.
 
 
+def _force_vscmd_skip_sendtelemetry(env):
+
+    if 'VSCMD_SKIP_SENDTELEMETRY' in env['ENV']:
+        return False
+
+    env['ENV']['VSCMD_SKIP_SENDTELEMETRY'] = '1'
+    debug("force env['ENV']['VSCMD_SKIP_SENDTELEMETRY']=%s", env['ENV']['VSCMD_SKIP_SENDTELEMETRY'])
+
+    return True
+
+
 def normalize_env(env, keys, force: bool=False):
     """Given a dictionary representing a shell environment, add the variables
     from os.environ needed for the processing of .bat files; the keys are
@@ -256,7 +268,7 @@ def normalize_env(env, keys, force: bool=False):
     return normenv
 
 
-def get_output(vcbat, args=None, env=None):
+def get_output(vcbat, args=None, env=None, skip_sendtelemetry=False):
     """Parse the output of given bat file, with given args."""
 
     if env is None:
@@ -295,20 +307,23 @@ def get_output(vcbat, args=None, env=None):
     ]
     env['ENV'] = normalize_env(env['ENV'], vs_vc_vars, force=False)
 
+    if skip_sendtelemetry:
+        _force_vscmd_skip_sendtelemetry(env)
+
     if args:
         debug("Calling '%s %s'", vcbat, args)
-        popen = SCons.Action._subproc(env,
-                                      '"%s" %s & set' % (vcbat, args),
-                                      stdin='devnull',
-                                      stdout=subprocess.PIPE,
-                                      stderr=subprocess.PIPE)
+        cmd_str = '"%s" %s & set' % (vcbat, args)
     else:
         debug("Calling '%s'", vcbat)
-        popen = SCons.Action._subproc(env,
-                                      '"%s" & set' % vcbat,
-                                      stdin='devnull',
-                                      stdout=subprocess.PIPE,
-                                      stderr=subprocess.PIPE)
+        cmd_str = '"%s" & set' % vcbat
+
+    popen = SCons.Action._subproc(
+        env,
+        cmd_str,
+        stdin='devnull',
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE
+    )
 
     # Use the .stdout and .stderr attributes directly because the
     # .communicate() method uses the threading module on Windows
diff --git a/SCons/Tool/MSCommon/vc.py b/SCons/Tool/MSCommon/vc.py
index 1dc6b8d111..d7da4ea922 100644
--- a/SCons/Tool/MSCommon/vc.py
+++ b/SCons/Tool/MSCommon/vc.py
@@ -43,6 +43,7 @@
 import subprocess
 import os
 import platform
+import sysconfig
 from pathlib import Path
 from string import digits as string_digits
 from subprocess import PIPE
@@ -69,8 +70,7 @@
     MSVCToolsetVersionNotFound,
 )
 
-class UnsupportedVersion(VisualCException):
-    pass
+# external exceptions
 
 class MSVCUnsupportedHostArch(VisualCException):
     pass
@@ -78,21 +78,35 @@ class MSVCUnsupportedHostArch(VisualCException):
 class MSVCUnsupportedTargetArch(VisualCException):
     pass
 
-class MissingConfiguration(VisualCException):
+class MSVCScriptNotFound(MSVCUserError):
     pass
 
-class NoVersionFound(VisualCException):
+class MSVCUseSettingsError(MSVCUserError):
     pass
 
-class BatchFileExecutionError(VisualCException):
+# internal exceptions
+
+class UnsupportedVersion(VisualCException):
     pass
 
-class MSVCScriptNotFound(MSVCUserError):
+class MissingConfiguration(VisualCException):
     pass
 
-class MSVCUseSettingsError(MSVCUserError):
+class BatchFileExecutionError(VisualCException):
     pass
 
+# undefined object for dict.get() in case key exists and value is None
+UNDEFINED = object()
+
+# powershell error sending telemetry for arm32 process on arm64 host (VS2019+):
+#    True:  force VSCMD_SKIP_SENDTELEMETRY=1 (if necessary)
+#    False: do nothing
+_ARM32_ON_ARM64_SKIP_SENDTELEMETRY = True
+
+# MSVC 9.0 preferred query order:
+#     True:  VCForPython, VisualStudio
+#     FAlse: VisualStudio, VCForPython
+_VC90_Prefer_VCForPython = True
 
 # Dict to 'canonalize' the arch
 _ARCH_TO_CANONICAL = {
@@ -282,7 +296,9 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
 # The cl path fragment under the toolset version folder is the second value of
 # the stored tuple.
 
-_GE2017_HOST_TARGET_BATCHFILE_CLPATHCOMPS = {
+# 14.3 (VS2022) and later
+
+_GE2022_HOST_TARGET_BATCHFILE_CLPATHCOMPS = {
 
     ('amd64', 'amd64') : ('vcvars64.bat',          ('bin', 'Hostx64', 'x64')),
     ('amd64', 'x86')   : ('vcvarsamd64_x86.bat',   ('bin', 'Hostx64', 'x86')),
@@ -294,11 +310,66 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
     ('x86',   'arm')   : ('vcvarsx86_arm.bat',     ('bin', 'Hostx86', 'arm')),
     ('x86',   'arm64') : ('vcvarsx86_arm64.bat',   ('bin', 'Hostx86', 'arm64')),
 
+    ('arm64', 'amd64') : ('vcvarsarm64_amd64.bat', ('bin', 'Hostarm64', 'arm64_amd64')),
+    ('arm64', 'x86')   : ('vcvarsarm64_x86.bat',   ('bin', 'Hostarm64', 'arm64_x86')),
+    ('arm64', 'arm')   : ('vcvarsarm64_arm.bat',   ('bin', 'Hostarm64', 'arm64_arm')),
+    ('arm64', 'arm64') : ('vcvarsarm64.bat',       ('bin', 'Hostarm64', 'arm64')),
+
 }
 
-_GE2017_HOST_TARGET_CFG = _host_target_config_factory(
+_GE2022_HOST_TARGET_CFG = _host_target_config_factory(
 
-    label = 'GE2017',
+    label = 'GE2022',
+
+    host_all_hosts = OrderedDict([
+        ('amd64', ['amd64', 'x86']),
+        ('x86',   ['x86']),
+        ('arm64', ['arm64', 'amd64', 'x86']),
+        ('arm',   ['x86']),
+    ]),
+
+    host_all_targets = {
+        'amd64': ['amd64', 'x86', 'arm64', 'arm'],
+        'x86':   ['x86', 'amd64', 'arm', 'arm64'],
+        'arm64': ['arm64', 'amd64', 'arm', 'x86'],
+        'arm':   [],
+    },
+
+    host_def_targets = {
+        'amd64': ['amd64', 'x86'],
+        'x86':   ['x86'],
+        'arm64': ['arm64', 'amd64', 'arm', 'x86'],
+        'arm':   ['arm'],
+    },
+
+)
+
+# debug("_GE2022_HOST_TARGET_CFG: %s", _GE2022_HOST_TARGET_CFG)
+
+# 14.2 (VS2019) to 14.1 (VS2017)
+
+_LE2019_HOST_TARGET_BATCHFILE_CLPATHCOMPS = {
+
+    ('amd64', 'amd64') : ('vcvars64.bat',          ('bin', 'Hostx64', 'x64')),
+    ('amd64', 'x86')   : ('vcvarsamd64_x86.bat',   ('bin', 'Hostx64', 'x86')),
+    ('amd64', 'arm')   : ('vcvarsamd64_arm.bat',   ('bin', 'Hostx64', 'arm')),
+    ('amd64', 'arm64') : ('vcvarsamd64_arm64.bat', ('bin', 'Hostx64', 'arm64')),
+
+    ('x86',   'amd64') : ('vcvarsx86_amd64.bat',   ('bin', 'Hostx86', 'x64')),
+    ('x86',   'x86')   : ('vcvars32.bat',          ('bin', 'Hostx86', 'x86')),
+    ('x86',   'arm')   : ('vcvarsx86_arm.bat',     ('bin', 'Hostx86', 'arm')),
+    ('x86',   'arm64') : ('vcvarsx86_arm64.bat',   ('bin', 'Hostx86', 'arm64')),
+
+    ('arm64', 'amd64') : ('vcvars64.bat',          ('bin', 'Hostx64', 'x64')),
+    ('arm64', 'x86')   : ('vcvarsamd64_x86.bat',   ('bin', 'Hostx64', 'x86')),
+    ('arm64', 'arm')   : ('vcvarsamd64_arm.bat',   ('bin', 'Hostx64', 'arm')),
+    ('arm64', 'arm64') : ('vcvarsamd64_arm64.bat', ('bin', 'Hostx64', 'arm64')),
+
+}
+
+_LE2019_HOST_TARGET_CFG = _host_target_config_factory(
+
+    label = 'LE2019',
 
     host_all_hosts = OrderedDict([
         ('amd64', ['amd64', 'x86']),
@@ -310,20 +381,20 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
     host_all_targets = {
         'amd64': ['amd64', 'x86', 'arm64', 'arm'],
         'x86':   ['x86', 'amd64', 'arm', 'arm64'],
-        'arm64': [],
+        'arm64': ['arm64', 'amd64', 'arm', 'x86'],
         'arm':   [],
     },
 
     host_def_targets = {
         'amd64': ['amd64', 'x86'],
         'x86':   ['x86'],
-        'arm64': ['arm64', 'arm'],
+        'arm64': ['arm64', 'amd64', 'arm', 'x86'],
         'arm':   ['arm'],
     },
 
 )
 
-# debug("_GE2017_HOST_TARGET_CFG: %s", _GE2017_HOST_TARGET_CFG)
+# debug("_LE2019_HOST_TARGET_CFG: %s", _LE2019_HOST_TARGET_CFG)
 
 # 14.0 (VS2015) to 8.0 (VS2005)
 
@@ -345,6 +416,10 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
     ('x86',   'arm')   : ('x86_arm',   ('bin', 'x86_arm')),
     ('x86',   'ia64')  : ('x86_ia64',  ('bin', 'x86_ia64')),
 
+    ('arm64', 'amd64') : ('amd64',     ('bin', 'amd64')),
+    ('arm64', 'x86')   : ('amd64_x86', ('bin', 'amd64_x86')),
+    ('arm64', 'arm')   : ('amd64_arm', ('bin', 'amd64_arm')),
+
     ('arm',   'arm')   : ('arm',       ('bin', 'arm')),
     ('ia64',  'ia64')  : ('ia64',      ('bin', 'ia64')),
 
@@ -357,6 +432,7 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
     host_all_hosts = OrderedDict([
         ('amd64', ['amd64', 'x86']),
         ('x86',   ['x86']),
+        ('arm64', ['amd64', 'x86']),
         ('arm',   ['arm']),
         ('ia64',  ['ia64']),
     ]),
@@ -364,6 +440,7 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
     host_all_targets = {
         'amd64': ['amd64', 'x86', 'arm'],
         'x86':   ['x86', 'amd64', 'arm', 'ia64'],
+        'arm64': ['amd64', 'x86', 'arm'],
         'arm':   ['arm'],
         'ia64':  ['ia64'],
     },
@@ -371,6 +448,7 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
     host_def_targets = {
         'amd64': ['amd64', 'x86'],
         'x86':   ['x86'],
+        'arm64': ['amd64', 'arm', 'x86'],
         'arm':   ['arm'],
         'ia64':  ['ia64'],
     },
@@ -391,16 +469,19 @@ def _make_target_host_map(all_hosts, host_all_targets_map):
     host_all_hosts = OrderedDict([
         ('amd64', ['x86']),
         ('x86',   ['x86']),
+        ('arm64', ['x86']),
     ]),
 
     host_all_targets = {
         'amd64': ['x86'],
         'x86':   ['x86'],
+        'arm64': ['x86'],
     },
 
     host_def_targets = {
         'amd64': ['x86'],
         'x86':   ['x86'],
+        'arm64': ['x86'],
     },
 
 )
@@ -444,28 +525,54 @@ def get_host_platform(host_platform):
 
     return host
 
+_native_host_architecture = None
+
+def get_native_host_architecture():
+    """Return the native host architecture."""
+    global _native_host_architecture
+
+    if _native_host_architecture is None:
+
+        try:
+            arch = common.read_reg(
+                r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment\PROCESSOR_ARCHITECTURE'
+            )
+        except OSError:
+            arch = None
+
+        if not arch:
+            arch = platform.machine()
+
+        _native_host_architecture = arch
+
+    return _native_host_architecture
+
 _native_host_platform = None
 
 def get_native_host_platform():
     global _native_host_platform
 
     if _native_host_platform is None:
-
-        _native_host_platform = get_host_platform(platform.machine())
+        arch = get_native_host_architecture()
+        _native_host_platform = get_host_platform(arch)
 
     return _native_host_platform
 
 def get_host_target(env, msvc_version, all_host_targets: bool=False):
 
     vernum = float(get_msvc_version_numeric(msvc_version))
-
-    if vernum > 14:
-        # 14.1 (VS2017) and later
-        host_target_cfg = _GE2017_HOST_TARGET_CFG
-    elif 14 >= vernum >= 8:
+    vernum_int = int(vernum * 10)
+
+    if vernum_int >= 143:
+        # 14.3 (VS2022) and later
+        host_target_cfg = _GE2022_HOST_TARGET_CFG
+    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)
         host_target_cfg = _LE2015_HOST_TARGET_CFG
-    else:
+    else: # 80 > vernum_int
         # 7.1 (VS2003) and earlier
         host_target_cfg = _LE2003_HOST_TARGET_CFG
 
@@ -520,6 +627,53 @@ def get_host_target(env, msvc_version, all_host_targets: bool=False):
 
     return host_platform, target_platform, host_target_list
 
+_arm32_process_arm64_host = None
+
+def is_arm32_process_arm64_host():
+    global _arm32_process_arm64_host
+
+    if _arm32_process_arm64_host is None:
+
+        host = get_native_host_architecture()
+        host = _ARCH_TO_CANONICAL.get(host.lower(),'')
+        host_isarm64 = host == 'arm64'
+
+        process = sysconfig.get_platform()
+        process_isarm32 = process == 'win-arm32'
+
+        _arm32_process_arm64_host = host_isarm64 and process_isarm32
+
+    return _arm32_process_arm64_host
+
+_check_skip_sendtelemetry = None
+
+def _skip_sendtelemetry(env):
+    global _check_skip_sendtelemetry
+
+    if _check_skip_sendtelemetry is None:
+
+        if _ARM32_ON_ARM64_SKIP_SENDTELEMETRY and is_arm32_process_arm64_host():
+            _check_skip_sendtelemetry = True
+        else:
+            _check_skip_sendtelemetry = False
+
+    if not _check_skip_sendtelemetry:
+        return False
+
+    msvc_version = env.get('MSVC_VERSION') if env else None
+    if not msvc_version:
+        msvc_version = msvc_default_version(env)
+
+    if not msvc_version:
+        return False
+
+    vernum = float(get_msvc_version_numeric(msvc_version))
+    if vernum < 14.2:  # VS2019
+        return False
+
+    # arm32 process, arm64 host, VS2019+
+    return True
+
 # If you update this, update SupportedVSList in Tool/MSCommon/vs.py, and the
 # MSVC_VERSION documentation in Tool/msvc.xml.
 _VCVER = [
@@ -589,6 +743,9 @@ def get_host_target(env, msvc_version, all_host_targets: bool=False):
     '9.0': [
         (SCons.Util.HKEY_CURRENT_USER, r'Microsoft\DevDiv\VCForPython\9.0\installdir',),
         (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',),
     ],
     '9.0Exp': [
         (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\9.0\Setup\VC\ProductDir'),
@@ -763,7 +920,7 @@ def find_vc_pdir(env, msvc_version):
                 raise MissingConfiguration("registry dir {} not found on the filesystem".format(comps))
     return None
 
-def find_batch_file(env, msvc_version, host_arch, target_arch):
+def find_batch_file(msvc_version, host_arch, target_arch, pdir):
     """
     Find the location of the batch script which should set up the compiler
     for any TARGET_ARCH whose compilers were installed by Visual Studio/VCExpress
@@ -772,52 +929,73 @@ def find_batch_file(env, msvc_version, host_arch, target_arch):
     scripts named with a host_target pair that calls vcvarsall.bat properly,
     so use that and return an empty argument.
     """
-    pdir = find_vc_pdir(env, msvc_version)
-    if pdir is None:
-        raise NoVersionFound("No version of Visual Studio found")
-    debug('looking in %s', pdir)
 
     # filter out e.g. "Exp" from the version name
-    msvc_ver_numeric = get_msvc_version_numeric(msvc_version)
-    vernum = float(msvc_ver_numeric)
+    vernum = float(get_msvc_version_numeric(msvc_version))
+    vernum_int = int(vernum * 10)
+
+    sdk_pdir = pdir
 
     arg = ''
     vcdir = None
+    clexe = None
 
-    if vernum > 14:
-        # 14.1 (VS2017) and later
+    if vernum_int >= 143:
+        # 14.3 (VS2022) and later
+        batfiledir = os.path.join(pdir, "Auxiliary", "Build")
+        batfile, _ = _GE2022_HOST_TARGET_BATCHFILE_CLPATHCOMPS[(host_arch, target_arch)]
+        batfilename = os.path.join(batfiledir, batfile)
+        vcdir = pdir
+    elif 143 > vernum_int >= 141:
+        # 14.2 (VS2019) to 14.1 (VS2017)
         batfiledir = os.path.join(pdir, "Auxiliary", "Build")
-        batfile, _ = _GE2017_HOST_TARGET_BATCHFILE_CLPATHCOMPS[(host_arch, target_arch)]
+        batfile, _ = _LE2019_HOST_TARGET_BATCHFILE_CLPATHCOMPS[(host_arch, target_arch)]
         batfilename = os.path.join(batfiledir, batfile)
         vcdir = pdir
-    elif 14 >= vernum >= 8:
+    elif 141 > vernum_int >= 80:
         # 14.0 (VS2015) to 8.0 (VS2005)
-        arg, _ = _LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS[(host_arch, target_arch)]
+        arg, cl_path_comps = _LE2015_HOST_TARGET_BATCHARG_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"))
-    else:
+            # Visual C++ for Python sdk batch files do not point to the VCForPython installation
+            sdk_pdir = None
+        clexe = os.path.join(pdir, *cl_path_comps, _CL_EXE_NAME)
+    else:  # 80 > vernum_int
         # 7.1 (VS2003) and earlier
         pdir = os.path.join(pdir, "Bin")
         batfilename = os.path.join(pdir, "vcvars32.bat")
+        clexe = os.path.join(pdir, _CL_EXE_NAME)
 
     if not os.path.exists(batfilename):
-        debug("Not found: %s", batfilename)
+        debug("batch file not found: %s", batfilename)
         batfilename = None
 
+    if clexe and not os.path.exists(clexe):
+        debug("cl.exe not found: %s", clexe)
+        batfilename = None
+
+    return batfilename, arg, vcdir, sdk_pdir
+
+def find_batch_file_sdk(host_arch, target_arch, sdk_pdir):
+    """
+    Find the location of the sdk batch script which should set up the compiler
+    for any TARGET_ARCH whose compilers were installed by Visual Studio/VCExpress
+    """
+
     installed_sdks = get_installed_sdks()
     for _sdk in installed_sdks:
         sdk_bat_file = _sdk.get_sdk_vc_script(host_arch, target_arch)
         if not sdk_bat_file:
-            debug("batch file not found:%s", _sdk)
+            debug("sdk batch file not found:%s", _sdk)
         else:
-            sdk_bat_file_path = os.path.join(pdir, sdk_bat_file)
+            sdk_bat_file_path = os.path.join(sdk_pdir, sdk_bat_file)
             if os.path.exists(sdk_bat_file_path):
                 debug('sdk_bat_file_path:%s', sdk_bat_file_path)
-                return batfilename, arg, vcdir, sdk_bat_file_path
+                return sdk_bat_file_path
 
-    return batfilename, arg, vcdir, None
+    return None
 
 __INSTALLED_VCS_RUN = None
 _VC_TOOLS_VERSION_FILE_PATH = ['Auxiliary', 'Build', 'Microsoft.VCToolsVersion.default.txt']
@@ -852,9 +1030,10 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool:
     host_platform, target_platform, host_target_list = platforms
 
     vernum = float(get_msvc_version_numeric(msvc_version))
+    vernum_int = int(vernum * 10)
 
     # make sure the cl.exe exists meaning the tool is installed
-    if vernum > 14:
+    if vernum_int >= 141:
         # 14.1 (VS2017) and later
         # 2017 and newer allowed multiple versions of the VC toolset to be
         # installed at the same time. This changes the layout.
@@ -871,11 +1050,18 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool:
             debug('failed to find MSVC version in %s', default_toolset_file)
             return False
 
+        if vernum_int >= 143:
+            # 14.3 (VS2022) and later
+            host_target_batchfile_clpathcomps = _GE2022_HOST_TARGET_BATCHFILE_CLPATHCOMPS
+        else:
+            # 14.2 (VS2019) to 14.1 (VS2017)
+            host_target_batchfile_clpathcomps = _LE2019_HOST_TARGET_BATCHFILE_CLPATHCOMPS
+
         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)
 
-            batchfile_clpathcomps = _GE2017_HOST_TARGET_BATCHFILE_CLPATHCOMPS.get((host_platform, target_platform), None)
+            batchfile_clpathcomps = host_target_batchfile_clpathcomps.get((host_platform, target_platform), None)
             if batchfile_clpathcomps is None:
                 debug('unsupported host/target platform combo: (%s,%s)', host_platform, target_platform)
                 continue
@@ -888,7 +1074,7 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool:
                 debug('found %s!', _CL_EXE_NAME)
                 return True
 
-    elif 14 >= vernum >= 8:
+    elif 141 > vernum_int >= 80:
         # 14.0 (VS2015) to 8.0 (VS2005)
 
         for host_platform, target_platform in host_target_list:
@@ -908,7 +1094,7 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool:
                 debug('found %s', _CL_EXE_NAME)
                 return True
 
-    elif 8 > vernum >= 6:
+    elif 80 > vernum_int >= 60:
         # 7.1 (VS2003) to 6.0 (VS6)
 
         # quick check for vc_dir/bin and vc_dir/ before walk
@@ -928,7 +1114,7 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool:
         return False
 
     else:
-        # version not support return false
+        # version not supported return false
         debug('unsupported MSVC version: %s', str(vernum))
 
     return False
@@ -939,6 +1125,13 @@ def get_installed_vcs(env=None):
     if __INSTALLED_VCS_RUN is not None:
         return __INSTALLED_VCS_RUN
 
+    save_target_arch = env.get('TARGET_ARCH', UNDEFINED) if env else None
+    force_target = env and save_target_arch and save_target_arch != UNDEFINED
+
+    if force_target:
+        del env['TARGET_ARCH']
+        debug("delete env['TARGET_ARCH']")
+
     installed_versions = []
 
     for ver in _VCVER:
@@ -960,7 +1153,12 @@ def get_installed_vcs(env=None):
         except VisualCException as e:
             debug('did not find VC %s: caught exception %s', ver, str(e))
 
+    if force_target:
+        env['TARGET_ARCH'] = save_target_arch
+        debug("restore env['TARGET_ARCH']=%s", save_target_arch)
+
     __INSTALLED_VCS_RUN = installed_versions
+    debug("__INSTALLED_VCS_RUN=%s", __INSTALLED_VCS_RUN)
     return __INSTALLED_VCS_RUN
 
 def reset_installed_vcs() -> None:
@@ -982,6 +1180,19 @@ def get_installed_vcs_components(env=None):
     msvc_version_component_defs = [MSVC.Util.msvc_version_components(vcver) for vcver in vcs]
     return msvc_version_component_defs
 
+def _check_cl_exists_in_script_env(data):
+    """Find cl.exe in the script environment path."""
+    cl_path = None
+    if data and 'PATH' in data:
+        for p in data['PATH']:
+            cl_exe = os.path.join(p, _CL_EXE_NAME)
+            if os.path.exists(cl_exe):
+                cl_path = cl_exe
+                break
+    have_cl = True if cl_path else False
+    debug('have_cl: %s, cl_path: %s', have_cl, cl_path)
+    return have_cl, cl_path
+
 # Running these batch files isn't cheap: most of the time spent in
 # msvs.generate() is due to vcvars*.bat.  In a build that uses "tools='msvs'"
 # in multiple environments, for example:
@@ -1027,7 +1238,8 @@ def script_env(env, script, args=None):
                     cache_data = None
 
     if cache_data is None:
-        stdout = common.get_output(script, args)
+        skip_sendtelemetry = _skip_sendtelemetry(env)
+        stdout = common.get_output(script, args, skip_sendtelemetry=skip_sendtelemetry)
         cache_data = common.parse_output(stdout)
 
         # debug(stdout)
@@ -1044,12 +1256,7 @@ def script_env(env, script, args=None):
         if script_errlog:
             script_errmsg = '\n'.join(script_errlog)
 
-            have_cl = False
-            if cache_data and 'PATH' in cache_data:
-                for p in cache_data['PATH']:
-                    if os.path.exists(os.path.join(p, _CL_EXE_NAME)):
-                        have_cl = True
-                        break
+            have_cl, _ = _check_cl_exists_in_script_env(cache_data)
 
             debug(
                 'script=%s args=%s have_cl=%s, errors=%s',
@@ -1130,6 +1337,18 @@ def msvc_find_valid_batch_script(env, version):
     get it right.
     """
 
+    # Find the product directory
+    pdir = None
+    try:
+        pdir = find_vc_pdir(env, version)
+    except UnsupportedVersion:
+        # Unsupported msvc version (raise MSVCArgumentError?)
+        pass
+    except MissingConfiguration:
+        # Found version, directory missing
+        pass
+    debug('product directory: version=%s, pdir=%s', version, pdir)
+
     # Find the host, target, and all candidate (host, target) platform combinations:
     platforms = get_host_target(env, version)
     debug("host_platform %s, target_platform %s host_target_list %s", *platforms)
@@ -1137,48 +1356,81 @@ def msvc_find_valid_batch_script(env, version):
 
     d = None
     version_installed = False
-    for host_arch, target_arch, in host_target_list:
-        # Set to current arch.
-        env['TARGET_ARCH'] = target_arch
-        arg = ''
 
-        # Try to locate a batch file for this host/target platform combo
-        try:
-            (vc_script, arg, vc_dir, sdk_script) = find_batch_file(env, version, host_arch, target_arch)
-            debug('vc_script:%s vc_script_arg:%s sdk_script:%s', vc_script, arg, sdk_script)
-            version_installed = True
-        except VisualCException as e:
-            msg = str(e)
-            debug('Caught exception while looking for batch file (%s)', msg)
-            version_installed = False
-            continue
+    if pdir:
+
+        # Query all candidate sdk (host, target, sdk_pdir) after vc_script pass if necessary
+        sdk_queries = []
+
+        for host_arch, target_arch, in host_target_list:
+            # Set to current arch.
+            env['TARGET_ARCH'] = target_arch
+            arg = ''
+
+            # Try to locate a batch file for this host/target platform combo
+            try:
+                (vc_script, arg, vc_dir, sdk_pdir) = find_batch_file(version, host_arch, target_arch, pdir)
+                debug('vc_script:%s vc_script_arg:%s', vc_script, arg)
+                version_installed = True
+            except VisualCException as e:
+                msg = str(e)
+                debug('Caught exception while looking for batch file (%s)', msg)
+                version_installed = False
+                continue
+
+            # Save (host, target, sdk_pdir) platform combo for sdk queries
+            if sdk_pdir:
+                sdk_query = (host_arch, target_arch, sdk_pdir)
+                if sdk_query not in sdk_queries:
+                    debug('save sdk_query host=%s, target=%s, sdk_pdir=%s', host_arch, target_arch, sdk_pdir)
+                    sdk_queries.append(sdk_query)
+
+            if not vc_script:
+                continue
 
-        # Try to use the located batch file for this host/target platform combo
-        debug('use_script 2 %s, args:%s', repr(vc_script), arg)
-        found = None
-        if vc_script:
+            # 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)
             try:
                 d = script_env(env, vc_script, args=arg)
-                found = vc_script
             except BatchFileExecutionError as e:
-                debug('use_script 3: failed running VC script %s: %s: Error:%s', repr(vc_script), arg, e)
-                vc_script=None
+                debug('failed vc_script:%s, vc_script_args:%s, error:%s', repr(vc_script), arg, e)
+                vc_script = None
                 continue
-        if not vc_script and sdk_script:
-            debug('use_script 4: trying sdk script: %s', sdk_script)
-            try:
-                d = script_env(env, sdk_script)
-                found = sdk_script
-            except BatchFileExecutionError as e:
-                debug('use_script 5: failed running SDK script %s: Error:%s', repr(sdk_script), e)
+
+            have_cl, _ = _check_cl_exists_in_script_env(d)
+            if not have_cl:
+                debug('skip cl.exe not found vc_script:%s, vc_script_args:%s', repr(vc_script), arg)
                 continue
-        elif not vc_script and not sdk_script:
-            debug('use_script 6: Neither VC script nor SDK script found')
-            continue
 
-        debug("Found a working script/target: %s/%s", repr(found), arg)
-        break # We've found a working target_platform, so stop looking
+            debug("Found a working script/target: %s/%s", repr(vc_script), arg)
+            break # We've found a working target_platform, so stop looking
+
+        if not d:
+            for host_arch, target_arch, sdk_pdir in sdk_queries:
+                # Set to current arch.
+                env['TARGET_ARCH'] = target_arch
+
+                sdk_script = find_batch_file_sdk(host_arch, target_arch, sdk_pdir)
+                if not sdk_script:
+                    continue
+
+                # Try to use the sdk batch file for this (host, target, sdk_pdir) combo
+                debug('trying sdk_script:%s', repr(sdk_script))
+                try:
+                    d = script_env(env, sdk_script)
+                    version_installed = True
+                except BatchFileExecutionError as e:
+                    debug('failed sdk_script:%s, error=%s', repr(sdk_script), e)
+                    continue
+
+                have_cl, _ = _check_cl_exists_in_script_env(d)
+                if not have_cl:
+                    debug('skip cl.exe not found sdk_script:%s', repr(sdk_script))
+                    continue
+
+                debug("Found a working script/target: %s", repr(sdk_script))
+                break # We've found a working script, so stop looking
 
     # If we cannot find a viable installed compiler, reset the TARGET_ARCH
     # To it's initial value
@@ -1206,8 +1458,6 @@ def msvc_find_valid_batch_script(env, version):
 
     return d
 
-_UNDEFINED = object()
-
 def get_use_script_use_settings(env):
 
     #   use_script  use_settings   return values   action
@@ -1217,9 +1467,9 @@ def get_use_script_use_settings(env):
 
     # None (documentation) or evaluates False (code): bypass detection
     # need to distinguish between undefined and None
-    use_script = env.get('MSVC_USE_SCRIPT', _UNDEFINED)
+    use_script = env.get('MSVC_USE_SCRIPT', UNDEFINED)
 
-    if use_script != _UNDEFINED:
+    if use_script != UNDEFINED:
         # use_script defined, use_settings ignored (not type checked)
         return use_script, None
 
@@ -1293,8 +1543,7 @@ def msvc_exists(env=None, version=None):
         rval = len(vcs) > 0
     else:
         rval = version in vcs
-    if not rval:
-        debug('version=%s, return=%s', repr(version), rval)
+    debug('version=%s, return=%s', repr(version), rval)
     return rval
 
 def msvc_setup_env_user(env=None):
diff --git a/SCons/Tool/MSCommon/vcTests.py b/SCons/Tool/MSCommon/vcTests.py
index c4cf2af699..9c2bd0ac8c 100644
--- a/SCons/Tool/MSCommon/vcTests.py
+++ b/SCons/Tool/MSCommon/vcTests.py
@@ -132,12 +132,48 @@ def runTest(self) -> None:
             print("Failed trying to write :%s :%s" % (tools_version_file, e))
 
 
-        # Now walk all the valid combinations of host/target for 14.1 (VS2017) and later
-        vc_ge2017_list = SCons.Tool.MSCommon.vc._GE2017_HOST_TARGET_CFG.all_pairs
+        # Test 14.3 (VS2022) and later
+        vc_ge2022_list = SCons.Tool.MSCommon.vc._GE2022_HOST_TARGET_CFG.all_pairs
 
-        for host, target in vc_ge2017_list:
-            batfile, clpathcomps = SCons.Tool.MSCommon.vc._GE2017_HOST_TARGET_BATCHFILE_CLPATHCOMPS[(host,target)]
-            # print("GT 14 Got: (%s, %s) -> (%s, %s)"%(host,target,batfile,clpathcomps))
+        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('.', 'Tools', 'MSVC', MS_TOOLS_VERSION, *clpathcomps)
+            MSVcTestCase._createDummyCl(path, 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)
@@ -173,7 +209,7 @@ def runTest(self) -> None:
 
         for host, target in vc_le2015_list:
             batarg, clpathcomps = SCons.Tool.MSCommon.vc._LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS[(host, target)]
-            # print("LE 14 Got: (%s, %s) -> (%s, %s)"%(host,target,batarg,clpathcomps))
+            # print("LE 14.0 Got: (%s, %s) -> (%s, %s)"%(host,target,batarg,clpathcomps))
             env={'TARGET_ARCH':target, 'HOST_ARCH':host}
             path = os.path.join('.', *clpathcomps)
             MSVcTestCase._createDummyCl(path, add_bin=False)
diff --git a/SCons/Tool/MSCommon/vs.py b/SCons/Tool/MSCommon/vs.py
index 00ccaef941..af0fd26e5a 100644
--- a/SCons/Tool/MSCommon/vs.py
+++ b/SCons/Tool/MSCommon/vs.py
@@ -207,7 +207,7 @@ def reset(self) -> None:
                  executable_path=r'Common7\IDE\devenv.com',
                  # should be a fallback, prefer use vswhere installationPath
                  batch_file_path=r'Common7\Tools\VsDevCmd.bat',
-                 supported_arch=['x86', 'amd64', "arm"],
+                 supported_arch=['x86', 'amd64', "arm", 'arm64'],
                  ),
 
     # Visual Studio 2019
@@ -219,7 +219,7 @@ def reset(self) -> None:
                  executable_path=r'Common7\IDE\devenv.com',
                  # should be a fallback, prefer use vswhere installationPath
                  batch_file_path=r'Common7\Tools\VsDevCmd.bat',
-                 supported_arch=['x86', 'amd64', "arm"],
+                 supported_arch=['x86', 'amd64', "arm", 'arm64'],
                  ),
 
     # Visual Studio 2017
@@ -231,7 +231,7 @@ def reset(self) -> None:
                  executable_path=r'Common7\IDE\devenv.com',
                  # should be a fallback, prefer use vswhere installationPath
                  batch_file_path=r'Common7\Tools\VsDevCmd.bat',
-                 supported_arch=['x86', 'amd64', "arm"],
+                 supported_arch=['x86', 'amd64', "arm", 'arm64'],
                  ),
 
     # Visual C++ 2017 Express Edition (for Desktop)
@@ -243,7 +243,7 @@ def reset(self) -> None:
                  executable_path=r'Common7\IDE\WDExpress.exe',
                  # should be a fallback, prefer use vswhere installationPath
                  batch_file_path=r'Common7\Tools\VsDevCmd.bat',
-                 supported_arch=['x86', 'amd64', "arm"],
+                 supported_arch=['x86', 'amd64', "arm", 'arm64'],
     ),
 
     # Visual Studio 2015
diff --git a/doc/generated/variables.gen b/doc/generated/variables.gen
index 42935a3170..86395a70e1 100644
--- a/doc/generated/variables.gen
+++ b/doc/generated/variables.gen
@@ -3179,8 +3179,8 @@ is <quote><literal>-dNOPAUSE -dBATCH -sDEVICE=pdfwrite</literal></quote>
       Valid host arch values are
       <literal>x86</literal> and <literal>arm</literal>
       for 32-bit hosts and
-      <literal>amd64</literal> and <literal>x86_64</literal>
-      for 64-bit hosts.
+      <literal>amd64</literal>, <literal>arm64</literal>,
+      and <literal>x86_64</literal> for 64-bit hosts.
     </para>
     <para>
       Should be considered immutable. 


Test patch:
diff --git a/testing/framework/TestSConsMSVS.py b/testing/framework/TestSConsMSVS.py
index 92e436d93a..8c34372b85 100644
--- a/testing/framework/TestSConsMSVS.py
+++ b/testing/framework/TestSConsMSVS.py
@@ -40,6 +40,10 @@
 import platform
 import traceback
 from xml.etree import ElementTree
+try:
+    import winreg
+except ImportError:
+    winreg = None
 
 import SCons.Errors
 from TestSCons import *
@@ -763,23 +767,42 @@ def run(self, *args, **kw):
         return result
 
     def get_vs_host_arch(self):
-        """ Returns an MSVS, SDK , and/or MSVS acceptable platform arch. """
+        """ Returns an MSVS, SDK, and/or MSVS acceptable platform arch. """
 
-        # Dict to 'canonalize' the arch
+        # Dict to 'canonicalize' the arch (synchronize with MSCommon\vc.py)
         _ARCH_TO_CANONICAL = {
-            "x86": "x86",
-            "amd64": "amd64",
-            "i386": "x86",
-            "emt64": "amd64",
-            "x86_64": "amd64",
-            "itanium": "ia64",
-            "ia64": "ia64",
+            "amd64"     : "amd64",
+            "emt64"     : "amd64",
+            "i386"      : "x86",
+            "i486"      : "x86",
+            "i586"      : "x86",
+            "i686"      : "x86",
+            "ia64"      : "ia64",      # deprecated
+            "itanium"   : "ia64",      # deprecated
+            "x86"       : "x86",
+            "x86_64"    : "amd64",
+            "arm"       : "arm",
+            "arm64"     : "arm64",
+            "aarch64"   : "arm64",
         }
 
-        host_platform = platform.machine()
+        host_platform = None
+
+        if winreg:
+            try:
+                winkey = winreg.OpenKeyEx(
+                    winreg.HKEY_LOCAL_MACHINE,
+                    r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
+                )
+                host_platform, _ = winreg.QueryValueEx(winkey, 'PROCESSOR_ARCHITECTURE')
+            except OSError:
+                pass
+
+        if not host_platform:
+            host_platform = platform.machine()
 
         try:
-            host = _ARCH_TO_CANONICAL[host_platform]
+            host = _ARCH_TO_CANONICAL[host_platform.lower()]
         except KeyError as e:
             # Default to x86 for all other platforms
             host = 'x86'