Skip to content

Commit e526491

Browse files
authored
Merge pull request #4720 from jcbrill/jbrill-msvc-slowtext
Rework MSVC environment powershell configuration and description in CHANGES.txt and RELEASE.txt
2 parents 2480274 + 3290ac7 commit e526491

File tree

3 files changed

+178
-37
lines changed

3 files changed

+178
-37
lines changed

CHANGES.txt

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,35 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
1616
- Whatever John Doe did.
1717

1818
From Joseph Brill:
19-
- MSVS: Fix a significant MSVC/MSVC tool initialization slowdown when
20-
vcpkg has been installed and the PSModulePath is not initialized
21-
or propagated from the user's shell environment. To resolve this
22-
The default Windows Powershell 7 path is added before the default
23-
Windows Powershell 5 path in the carefully constructed
24-
environment in which the MSVC batch files are run. The shell
25-
environment variables and values for VCPKG_DISABLE_METRICS and
26-
VCPKG_ROOT are added to the limited MSVC environment when defined.
27-
At present, the VCPKG_DISABLE_METRICS and VCPKG_ROOT variables and
28-
values are not propagated to the SCons environment after running
29-
the MSVC batch files.
19+
- MSVC: A significant delay was experienced in the Github Actions windows
20+
2022 and 2025 runners due to the environment used by SCons to initialize
21+
MSVC when the Visual Studio vcpkg component is installed. The Visual
22+
Studio vcpkg component is not installed in the Github Actions windows
23+
2019 runner.
24+
The Visual Studio vcpkg component invokes a powershell script when the
25+
MSVC batch files are called. The significant delay in the Github
26+
Actions windows 2022 and 2025 runners appears due to the environment
27+
used by SCons to initialize MSVC not including the pwsh executable on
28+
the system path, not including the powershell module analysis cache
29+
location, and not including the powershell module path.
30+
Adding the pwsh and powershell executable paths in the order discovered
31+
on the shell environment path, passing the powershell module analysis
32+
cache location, and adding a subset of the powershell module path to the
33+
environment used by SCons to initialize MSVC appears to have eliminated
34+
the significant delays in the Github Actions windows 2022 and 2025
35+
runners.
36+
In the Github Actions windows 2022 and 2025 runners, any one of the
37+
three additions appears to eliminate the significant delays. It is hoped
38+
that the combination of all three additions will guard against
39+
significant delays in other environment configurations as well.
40+
- MSVC: The following shell environment variables are now included in the
41+
environment used by SCons to initialize MSVC when defined:
42+
VCPKG_DISABLE_METRICS, VCPKG_ROOT, POWERSHELL_TELEMETRY_OPTOUT,
43+
PSDisableModuleAnalysisCacheCleanup, and PSModuleAnalysisCachePath. A
44+
subset of the shell environment PSModulePath is included in the
45+
environment used by SCons to initialize MSVC when defined. None of
46+
these variables and values are propagated to the user's SCons
47+
environment after running the MSVC batch files.
3048

3149
From Edward Peek:
3250
- Fix the variant dir component being missing from generated source file
@@ -82,7 +100,7 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
82100
- Ninja tool generate_command() fixed to call subst() with correct
83101
arguments in ListAction case. Unit tests added for generate_command.
84102
Fixes #4580.
85-
103+
86104
From Edward Peek:
87105
- Fix the variant dir component being missing from generated source file
88106
paths with CompilationDatabase() builder (Fixes #4003).

RELEASE.txt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,25 @@ CHANGED/ENHANCED EXISTING FUNCTIONALITY
3535
one from PEP 308 introduced in Python 2.5 (2006). The idiom being
3636
replaced (using and/or) is regarded as error prone.
3737

38-
- MSVS: The default Windows powershell 7 path is added before the default
39-
Windows powershell 5 path in the limited environment in which the
40-
MSVC batch files are run.
38+
- MSVC: The following shell environment variables are now included in
39+
the environment used by SCons to initialize MSVC when defined:
40+
VCPKG_DISABLE_METRICS, VCPKG_ROOT, POWERSHELL_TELEMETRY_OPTOUT,
41+
PSDisableModuleAnalysisCacheCleanup, and PSModuleAnalysisCachePath.
42+
A subset of the shell environment PSModulePath is included in the
43+
environment used by SCons to initialize MSVC when defined. None of
44+
these variables and values are propagated to the user's SCons
45+
environment after running the MSVC batch files.
4146

4247
FIXES
4348
-----
4449

4550
- Fixed SCons.Variables.PackageVariable to correctly test the default
4651
setting against both enable & disable strings. (Fixes #4702)
4752

48-
- MSVS: Fix significant slowdown initializing MSVC tools when vcpkg has
49-
been installed on the system.
53+
- MSVC: Fixed a significant delay experienced in the Github Actions
54+
windows 2022 and 2025 runners due to the environment used by SCons
55+
to initialize MSVC when the Visual Studio vcpkg component is
56+
installed. The Github Actions windows 2019 runner was not affected.
5057

5158
- Fix the variant dir component being missing from generated source file
5259
paths with CompilationDatabase() builder (Fixes #4003).

SCons/Tool/MSCommon/common.py

Lines changed: 136 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import os
3131
import re
3232
import sys
33+
import time
3334
from contextlib import suppress
3435
from subprocess import DEVNULL, PIPE
3536
from pathlib import Path
@@ -70,6 +71,9 @@
7071
'windir', # windows directory (SystemRoot not available in 95/98/ME)
7172
'VCPKG_DISABLE_METRICS',
7273
'VCPKG_ROOT',
74+
'POWERSHELL_TELEMETRY_OPTOUT',
75+
'PSDisableModuleAnalysisCacheCleanup',
76+
'PSModuleAnalysisCachePath',
7377
]
7478

7579
class MSVCCacheInvalidWarning(SCons.Warnings.WarningOnByDefault):
@@ -339,6 +343,112 @@ def _force_vscmd_skip_sendtelemetry(env):
339343
return True
340344

341345

346+
class _PathManager:
347+
348+
_PSEXECUTABLES = (
349+
"pwsh.exe",
350+
"powershell.exe",
351+
)
352+
353+
_PSMODULEPATH_MAP = {os.path.normcase(os.path.abspath(p)): p for p in [
354+
# os.path.expandvars(r"%USERPROFILE%\Documents\PowerShell\Modules"), # current user
355+
os.path.expandvars(r"%ProgramFiles%\PowerShell\Modules"), # all users
356+
os.path.expandvars(r"%ProgramFiles%\PowerShell\7\Modules"), # installation location
357+
# os.path.expandvars(r"%USERPROFILE%\Documents\WindowsPowerShell\Modules"), # current user
358+
os.path.expandvars(r"%ProgramFiles%\WindowsPowerShell\Modules"), # all users
359+
os.path.expandvars(r"%windir%\System32\WindowsPowerShell\v1.0\Modules"), # installation location
360+
]}
361+
362+
_cache_norm_path = {}
363+
364+
@classmethod
365+
def _get_norm_path(cls, p):
366+
norm_path = cls._cache_norm_path.get(p)
367+
if norm_path is None:
368+
norm_path = os.path.normcase(os.path.abspath(p))
369+
cls._cache_norm_path[p] = norm_path
370+
cls._cache_norm_path[norm_path] = norm_path
371+
return norm_path
372+
373+
_cache_is_psmodulepath = {}
374+
375+
@classmethod
376+
def _is_psmodulepath(cls, p):
377+
is_psmodulepath = cls._cache_is_psmodulepath.get(p)
378+
if is_psmodulepath is None:
379+
norm_path = cls._get_norm_path(p)
380+
is_psmodulepath = bool(norm_path in cls._PSMODULEPATH_MAP)
381+
cls._cache_is_psmodulepath[p] = is_psmodulepath
382+
cls._cache_is_psmodulepath[norm_path] = is_psmodulepath
383+
return is_psmodulepath
384+
385+
_cache_psmodulepath_paths = {}
386+
387+
@classmethod
388+
def get_psmodulepath_paths(cls, pathspec):
389+
psmodulepath_paths = cls._cache_psmodulepath_paths.get(pathspec)
390+
if psmodulepath_paths is None:
391+
psmodulepath_paths = []
392+
for p in pathspec.split(os.pathsep):
393+
p = p.strip()
394+
if not p:
395+
continue
396+
if not cls._is_psmodulepath(p):
397+
continue
398+
psmodulepath_paths.append(p)
399+
psmodulepath_paths = tuple(psmodulepath_paths)
400+
cls._cache_psmodulepath_paths[pathspec] = psmodulepath_paths
401+
return psmodulepath_paths
402+
403+
_cache_psexe_paths = {}
404+
405+
@classmethod
406+
def get_psexe_paths(cls, pathspec):
407+
psexe_paths = cls._cache_psexe_paths.get(pathspec)
408+
if psexe_paths is None:
409+
psexe_set = set(cls._PSEXECUTABLES)
410+
psexe_paths = []
411+
for p in pathspec.split(os.pathsep):
412+
p = p.strip()
413+
if not p:
414+
continue
415+
for psexe in psexe_set:
416+
psexe_path = os.path.join(p, psexe)
417+
if not os.path.exists(psexe_path):
418+
continue
419+
psexe_paths.append(p)
420+
psexe_set.remove(psexe)
421+
break
422+
if psexe_set:
423+
continue
424+
break
425+
psexe_paths = tuple(psexe_paths)
426+
cls._cache_psexe_paths[pathspec] = psexe_paths
427+
return psexe_paths
428+
429+
_cache_minimal_pathspec = {}
430+
431+
@classmethod
432+
def get_minimal_pathspec(cls, pathlist):
433+
pathlist_t = tuple(pathlist)
434+
minimal_pathspec = cls._cache_minimal_pathspec.get(pathlist_t)
435+
if minimal_pathspec is None:
436+
minimal_paths = []
437+
seen = set()
438+
for p in pathlist:
439+
p = p.strip()
440+
if not p:
441+
continue
442+
norm_path = cls._get_norm_path(p)
443+
if norm_path in seen:
444+
continue
445+
seen.add(norm_path)
446+
minimal_paths.append(p)
447+
minimal_pathspec = os.pathsep.join(minimal_paths)
448+
cls._cache_minimal_pathspec[pathlist_t] = minimal_pathspec
449+
return minimal_pathspec
450+
451+
342452
def normalize_env(env, keys, force: bool=False):
343453
"""Given a dictionary representing a shell environment, add the variables
344454
from os.environ needed for the processing of .bat files; the keys are
@@ -363,45 +473,46 @@ def normalize_env(env, keys, force: bool=False):
363473
else:
364474
debug("keys: skipped[%s]", k)
365475

476+
syspath_pathlist = normenv.get("PATH", "").split(os.pathsep)
477+
366478
# add some things to PATH to prevent problems:
367479
# Shouldn't be necessary to add system32, since the default environment
368480
# should include it, but keep this here to be safe (needed for reg.exe)
369481
sys32_dir = os.path.join(
370482
os.environ.get("SystemRoot", os.environ.get("windir", r"C:\Windows")), "System32"
371483
)
372-
if sys32_dir not in normenv["PATH"]:
373-
normenv["PATH"] = normenv["PATH"] + os.pathsep + sys32_dir
484+
syspath_pathlist.append(sys32_dir)
374485

375486
# Without Wbem in PATH, vcvarsall.bat has a "'wmic' is not recognized"
376487
# error starting with Visual Studio 2017, although the script still
377488
# seems to work anyway.
378489
sys32_wbem_dir = os.path.join(sys32_dir, 'Wbem')
379-
if sys32_wbem_dir not in normenv['PATH']:
380-
normenv['PATH'] = normenv['PATH'] + os.pathsep + sys32_wbem_dir
381-
382-
# ProgramFiles for PowerShell 7 Path and PSModulePath
383-
progfiles_dir = os.environ.get("ProgramFiles")
384-
if not progfiles_dir:
385-
sysroot_drive, _ = os.path.splitdrive(sys32_dir)
386-
sysroot_path = sysroot_drive + os.sep
387-
progfiles_dir = os.path.join(sysroot_path, "Program Files")
388-
389-
# Powershell 7
390-
progfiles_ps_dir = os.path.join(progfiles_dir, "PowerShell", "7")
391-
if progfiles_ps_dir not in normenv["PATH"]:
392-
normenv["PATH"] = normenv["PATH"] + os.pathsep + progfiles_ps_dir
490+
syspath_pathlist.append(sys32_wbem_dir)
393491

394492
# Without Powershell in PATH, an internal call to a telemetry
395493
# function (starting with a VS2019 update) can fail
396494
# Note can also set VSCMD_SKIP_SENDTELEMETRY to avoid this.
397-
sys32_ps_dir = os.path.join(sys32_dir, r'WindowsPowerShell\v1.0')
398-
if sys32_ps_dir not in normenv['PATH']:
399-
normenv['PATH'] = normenv['PATH'] + os.pathsep + sys32_ps_dir
400495

496+
# Find the powershell executable paths. Add the known powershell.exe
497+
# path to the end of the shell system path (just in case).
498+
# The VS vcpkg component prefers pwsh.exe if it's on the path.
499+
sys32_ps_dir = os.path.join(sys32_dir, 'WindowsPowerShell', 'v1.0')
500+
psexe_searchlist = os.pathsep.join([os.environ.get("PATH", ""), sys32_ps_dir])
501+
psexe_pathlist = _PathManager.get_psexe_paths(psexe_searchlist)
502+
503+
# Add powershell executable paths in the order discovered.
504+
syspath_pathlist.extend(psexe_pathlist)
505+
506+
normenv['PATH'] = _PathManager.get_minimal_pathspec(syspath_pathlist)
401507
debug("PATH: %s", normenv['PATH'])
402-
return normenv
403508

509+
# Add psmodulepath paths in the order discovered.
510+
psmodulepath_pathlist = _PathManager.get_psmodulepath_paths(os.environ.get("PSModulePath", ""))
511+
if psmodulepath_pathlist:
512+
normenv["PSModulePath"] = _PathManager.get_minimal_pathspec(psmodulepath_pathlist)
404513

514+
debug("PSModulePath: %s", normenv.get('PSModulePath',''))
515+
return normenv
405516

406517

407518
def get_output(vcbat, args=None, env=None, skip_sendtelemetry=False):
@@ -425,10 +536,15 @@ def get_output(vcbat, args=None, env=None, skip_sendtelemetry=False):
425536
debug("Calling '%s'", vcbat)
426537
cmd_str = '"%s" & set' % vcbat
427538

539+
beg_time = time.time()
540+
428541
cp = SCons.Action.scons_subproc_run(
429542
env, cmd_str, stdin=DEVNULL, stdout=PIPE, stderr=PIPE,
430543
)
431544

545+
end_time = time.time()
546+
debug("Elapsed %.2fs", end_time - beg_time)
547+
432548
# Extra debug logic, uncomment if necessary
433549
# debug('stdout:%s', cp.stdout)
434550
# debug('stderr:%s', cp.stderr)

0 commit comments

Comments
 (0)