MSVC: detection from registry (#38500)

Typically MSVC is detected via the VSWhere program. However, this may
not be available, or may be installed in an unpredictable location.
This PR adds an additional approach via Windows Registry queries to
determine VS install location root.

Additionally:

* Construct vs_install_paths after class-definition time (move it to
  variable-access time).
* Skip over keys for which a user does not have read permissions
  when performing searches (previously the presence of these keys
  would have caused an error, regardless of whether they were
  needed).
* Extend helper functionality with option for regex matching on
  registry keys vs. exact string matching.
* Some internal refactoring: remove boolean parameters in some cases
  where the function was always called with the same value
  (e.g. `find_subkey`)
This commit is contained in:
John W. Parent 2023-10-27 19:58:50 -04:00 committed by GitHub
parent 9e01199e13
commit 148dce96ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 169 additions and 56 deletions

View file

@ -269,7 +269,7 @@ def find_windows_compiler_root_paths() -> List[str]:
At the moment simply returns location of VS install paths from VSWhere At the moment simply returns location of VS install paths from VSWhere
But should be extended to include more information as relevant""" But should be extended to include more information as relevant"""
return list(winOs.WindowsOs.vs_install_paths) return list(winOs.WindowsOs().vs_install_paths)
@staticmethod @staticmethod
def find_windows_compiler_cmake_paths() -> List[str]: def find_windows_compiler_cmake_paths() -> List[str]:

View file

@ -5,10 +5,12 @@
import glob import glob
import os import os
import pathlib
import platform import platform
import subprocess import subprocess
from spack.error import SpackError from spack.error import SpackError
from spack.util import windows_registry as winreg
from spack.version import Version from spack.version import Version
from ._operating_system import OperatingSystem from ._operating_system import OperatingSystem
@ -31,43 +33,6 @@ class WindowsOs(OperatingSystem):
10. 10.
""" """
# Find MSVC directories using vswhere
comp_search_paths = []
vs_install_paths = []
root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
if root:
try:
extra_args = {"encoding": "mbcs", "errors": "strict"}
paths = subprocess.check_output( # type: ignore[call-overload] # novermin
[
os.path.join(root, "Microsoft Visual Studio", "Installer", "vswhere.exe"),
"-prerelease",
"-requires",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"-property",
"installationPath",
"-products",
"*",
],
**extra_args,
).strip()
vs_install_paths = paths.split("\n")
msvc_paths = [os.path.join(path, "VC", "Tools", "MSVC") for path in vs_install_paths]
for p in msvc_paths:
comp_search_paths.extend(glob.glob(os.path.join(p, "*", "bin", "Hostx64", "x64")))
if os.getenv("ONEAPI_ROOT"):
comp_search_paths.extend(
glob.glob(
os.path.join(
str(os.getenv("ONEAPI_ROOT")), "compiler", "*", "windows", "bin"
)
)
)
except (subprocess.CalledProcessError, OSError, UnicodeDecodeError):
pass
if comp_search_paths:
compiler_search_paths = comp_search_paths
def __init__(self): def __init__(self):
plat_ver = windows_version() plat_ver = windows_version()
if plat_ver < Version("10"): if plat_ver < Version("10"):
@ -76,3 +41,71 @@ def __init__(self):
def __str__(self): def __str__(self):
return self.name return self.name
@property
def vs_install_paths(self):
vs_install_paths = []
root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
if root:
try:
extra_args = {"encoding": "mbcs", "errors": "strict"}
paths = subprocess.check_output( # type: ignore[call-overload] # novermin
[
os.path.join(root, "Microsoft Visual Studio", "Installer", "vswhere.exe"),
"-prerelease",
"-requires",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"-property",
"installationPath",
"-products",
"*",
],
**extra_args,
).strip()
vs_install_paths = paths.split("\n")
except (subprocess.CalledProcessError, OSError, UnicodeDecodeError):
pass
return vs_install_paths
@property
def msvc_paths(self):
return [os.path.join(path, "VC", "Tools", "MSVC") for path in self.vs_install_paths]
@property
def compiler_search_paths(self):
# First Strategy: Find MSVC directories using vswhere
_compiler_search_paths = []
for p in self.msvc_paths:
_compiler_search_paths.extend(glob.glob(os.path.join(p, "*", "bin", "Hostx64", "x64")))
if os.getenv("ONEAPI_ROOT"):
_compiler_search_paths.extend(
glob.glob(
os.path.join(str(os.getenv("ONEAPI_ROOT")), "compiler", "*", "windows", "bin")
)
)
# Second strategy: Find MSVC via the registry
msft = winreg.WindowsRegistryView(
"SOFTWARE\\WOW6432Node\\Microsoft", winreg.HKEY.HKEY_LOCAL_MACHINE
)
vs_entries = msft.find_subkeys(r"VisualStudio_.*")
vs_paths = []
def clean_vs_path(path):
path = path.split(",")[0].lstrip("@")
return str((pathlib.Path(path).parent / "..\\..").resolve())
for entry in vs_entries:
try:
val = entry.get_subkey("Capabilities").get_value("ApplicationDescription").value
vs_paths.append(clean_vs_path(val))
except FileNotFoundError as e:
if hasattr(e, "winerror"):
if e.winerror == 2:
pass
else:
raise
else:
raise
_compiler_search_paths.extend(vs_paths)
return _compiler_search_paths

View file

@ -8,6 +8,7 @@
""" """
import os import os
import re
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
@ -68,8 +69,19 @@ def _gather_subkey_info(self):
sub_keys, _, _ = winreg.QueryInfoKey(self.hkey) sub_keys, _, _ = winreg.QueryInfoKey(self.hkey)
for i in range(sub_keys): for i in range(sub_keys):
sub_name = winreg.EnumKey(self.hkey, i) sub_name = winreg.EnumKey(self.hkey, i)
sub_handle = winreg.OpenKeyEx(self.hkey, sub_name, access=winreg.KEY_READ) try:
self._keys.append(RegistryKey(os.path.join(self.path, sub_name), sub_handle)) sub_handle = winreg.OpenKeyEx(self.hkey, sub_name, access=winreg.KEY_READ)
self._keys.append(RegistryKey(os.path.join(self.path, sub_name), sub_handle))
except OSError as e:
if hasattr(e, "winerror"):
if e.winerror == 5:
# This is a permission error, we can't read this key
# move on
pass
else:
raise
else:
raise
def _gather_value_info(self): def _gather_value_info(self):
"""Compose all values for this key into a dict of form value name: RegistryValue Object""" """Compose all values for this key into a dict of form value name: RegistryValue Object"""
@ -161,6 +173,15 @@ def __init__(self, key, root_key=HKEY.HKEY_CURRENT_USER):
self.root = root_key self.root = root_key
self._reg = None self._reg = None
class KeyMatchConditions:
@staticmethod
def regex_matcher(subkey_name):
return lambda x: re.match(subkey_name, x.name)
@staticmethod
def name_matcher(subkey_name):
return lambda x: subkey_name == x.name
@contextmanager @contextmanager
def invalid_reg_ref_error_handler(self): def invalid_reg_ref_error_handler(self):
try: try:
@ -193,6 +214,10 @@ def _valid_reg_check(self):
return False return False
return True return True
def _regex_match_subkeys(self, subkey):
r_subkey = re.compile(subkey)
return [key for key in self.get_subkeys() if r_subkey.match(key.name)]
@property @property
def reg(self): def reg(self):
if not self._reg: if not self._reg:
@ -218,51 +243,106 @@ def get_subkeys(self):
with self.invalid_reg_ref_error_handler(): with self.invalid_reg_ref_error_handler():
return self.reg.subkeys return self.reg.subkeys
def get_matching_subkeys(self, subkey_name):
"""Returns all subkeys regex matching subkey name
Note: this method obtains only direct subkeys of the given key and does not
desced to transtitve subkeys. For this behavior, see `find_matching_subkeys`"""
self._regex_match_subkeys(subkey_name)
def get_values(self): def get_values(self):
if not self._valid_reg_check(): if not self._valid_reg_check():
raise RegistryError("Cannot query values from invalid key %s" % self.key) raise RegistryError("Cannot query values from invalid key %s" % self.key)
with self.invalid_reg_ref_error_handler(): with self.invalid_reg_ref_error_handler():
return self.reg.values return self.reg.values
def _traverse_subkeys(self, stop_condition): def _traverse_subkeys(self, stop_condition, collect_all_matching=False):
"""Perform simple BFS of subkeys, returning the key """Perform simple BFS of subkeys, returning the key
that successfully triggers the stop condition. that successfully triggers the stop condition.
Args: Args:
stop_condition: lambda or function pointer that takes a single argument stop_condition: lambda or function pointer that takes a single argument
a key and returns a boolean value based on that key a key and returns a boolean value based on that key
collect_all_matching: boolean value, if True, the traversal collects and returns
all keys meeting stop condition. If false, once stop
condition is met, the key that triggered the condition '
is returned.
Return: Return:
the key if stop_condition is triggered, or None if not the key if stop_condition is triggered, or None if not
""" """
collection = []
if not self._valid_reg_check(): if not self._valid_reg_check():
raise RegistryError("Cannot query values from invalid key %s" % self.key) raise RegistryError("Cannot query values from invalid key %s" % self.key)
with self.invalid_reg_ref_error_handler(): with self.invalid_reg_ref_error_handler():
queue = self.reg.subkeys queue = self.reg.subkeys
for key in queue: for key in queue:
if stop_condition(key): if stop_condition(key):
return key if collect_all_matching:
collection.append(key)
else:
return key
queue.extend(key.subkeys) queue.extend(key.subkeys)
return None return collection if collection else None
def find_subkey(self, subkey_name, recursive=True): def _find_subkey_s(self, search_key, collect_all_matching=False):
"""If non recursive, this method is the same as get subkey with error handling """Retrieve one or more keys regex matching `search_key`.
Otherwise perform a BFS of subkeys until desired key is found One key will be returned unless `collect_all_matching` is enabled,
Returns None or RegistryKey object corresponding to requested key name in which case call matches are returned.
Args: Args:
subkey_name (str): string representing subkey to be searched for search_key (str): regex string represeting a subkey name structure
recursive (bool): optional argument, if True, subkey need not be a direct to be matched against.
sub key of this registry entry, and this method will Cannot be provided alongside `direct_subkey`
search all subkeys recursively. collect_all_matching (bool): No-op if `direct_subkey` is specified
Default is True
Return: Return:
the desired subkey as a RegistryKey object, or none the desired subkey as a RegistryKey object, or none
""" """
return self._traverse_subkeys(search_key, collect_all_matching=collect_all_matching)
if not recursive: def find_subkey(self, subkey_name):
return self.get_subkey(subkey_name) """Perform a BFS of subkeys until desired key is found
Returns None or RegistryKey object corresponding to requested key name
else: Args:
return self._traverse_subkeys(lambda x: x.name == subkey_name) subkey_name (str)
Return:
the desired subkey as a RegistryKey object, or none
For more details, see the WindowsRegistryView._find_subkey_s method docstring
"""
return self._find_subkey_s(
WindowsRegistryView.KeyMatchConditions.name_matcher(subkey_name)
)
def find_matching_subkey(self, subkey_name):
"""Perform a BFS of subkeys until a key matching subkey name regex is found
Returns None or the first RegistryKey object corresponding to requested key name
Args:
subkey_name (str)
Return:
the desired subkey as a RegistryKey object, or none
For more details, see the WindowsRegistryView._find_subkey_s method docstring
"""
return self._find_subkey_s(
WindowsRegistryView.KeyMatchConditions.regex_matcher(subkey_name)
)
def find_subkeys(self, subkey_name):
"""Exactly the same as find_subkey, except this function tries to match
a regex to multiple keys
Args:
subkey_name (str)
Return:
the desired subkeys as a list of RegistryKey object, or none
For more details, see the WindowsRegistryView._find_subkey_s method docstring
"""
kwargs = {"collect_all_matching": True}
return self._find_subkey_s(
WindowsRegistryView.KeyMatchConditions.regex_matcher(subkey_name), **kwargs
)
def find_value(self, val_name, recursive=True): def find_value(self, val_name, recursive=True):
""" """