3030import os
3131import re
3232import sys
33+ import time
3334from contextlib import suppress
3435from subprocess import DEVNULL , PIPE
3536from pathlib import Path
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
7579class 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+
342452def 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
407518def 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