summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJohn W. Parent <45471568+johnwparent@users.noreply.github.com>2022-11-22 03:27:42 -0500
committerGitHub <noreply@github.com>2022-11-22 00:27:42 -0800
commit793a7bc6a97d86b9b0a6da32c0e3694ddeaa6590 (patch)
treea999bd6e00e88fa91da3705cb7ef50e447f775f2 /lib
parent376afd631cc9217f137b63b7bb7f2f9275518cb6 (diff)
downloadspack-793a7bc6a97d86b9b0a6da32c0e3694ddeaa6590.tar.gz
spack-793a7bc6a97d86b9b0a6da32c0e3694ddeaa6590.tar.bz2
spack-793a7bc6a97d86b9b0a6da32c0e3694ddeaa6590.tar.xz
spack-793a7bc6a97d86b9b0a6da32c0e3694ddeaa6590.zip
Windows: add registry query and SDK/WDK packages (#33021)
* Add a WindowsRegistryView class, which can query for existing package installations on Windows. This is particularly important because some Windows packages (including those added here) do not allow two simultaneous installs, and this can be queried in order to provide a clear error message. * Consolidate external path detection logic for Windows into WindowsKitExternalPaths and WindowsCompilerExternalPaths objects. * Add external-only packages win-sdk and wgl * Add win-wdk (including external detection) which depends on win-sdk * Replace prior msmpi implementation with a source-based install (depends on win-wdk). This install can control the install destination (unlike the binary installation). * Update MSVC compiler to choose vcvars based on win-sdk dependency * Provide "msbuild" module-level variable to packages during build * When creating symlinks on Windows, need to explicitly specify when a symlink target is a directory * executables_in_path no-longer defaults to using PATH (this is now expected to be taken care of by the caller)
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/llnl/util/symlink.py2
-rw-r--r--lib/spack/spack/build_environment.py1
-rw-r--r--lib/spack/spack/compilers/msvc.py43
-rw-r--r--lib/spack/spack/detection/common.py171
-rw-r--r--lib/spack/spack/detection/path.py94
-rwxr-xr-xlib/spack/spack/operating_systems/windows_os.py14
-rw-r--r--lib/spack/spack/test/cmd/external.py4
-rw-r--r--lib/spack/spack/util/windows_registry.py291
8 files changed, 552 insertions, 68 deletions
diff --git a/lib/spack/llnl/util/symlink.py b/lib/spack/llnl/util/symlink.py
index 103c5b4c38..2b71441d4b 100644
--- a/lib/spack/llnl/util/symlink.py
+++ b/lib/spack/llnl/util/symlink.py
@@ -24,7 +24,7 @@ def symlink(real_path, link_path):
On Windows, use junctions if os.symlink fails.
"""
if not is_windows or _win32_can_symlink():
- os.symlink(real_path, link_path)
+ os.symlink(real_path, link_path, target_is_directory=os.path.isdir(real_path))
else:
try:
# Try to use junctions
diff --git a/lib/spack/spack/build_environment.py b/lib/spack/spack/build_environment.py
index 5b9120d2ff..262c2683b5 100644
--- a/lib/spack/spack/build_environment.py
+++ b/lib/spack/spack/build_environment.py
@@ -566,6 +566,7 @@ def _set_variables_for_single_module(pkg, module):
if sys.platform == "win32":
m.nmake = Executable("nmake")
+ m.msbuild = Executable("msbuild")
# Standard CMake arguments
m.std_cmake_args = spack.build_systems.cmake.CMakeBuilder.std_args(pkg)
m.std_meson_args = spack.build_systems.meson.MesonBuilder.std_args(pkg)
diff --git a/lib/spack/spack/compilers/msvc.py b/lib/spack/spack/compilers/msvc.py
index 110ef8099e..c79647b0bc 100644
--- a/lib/spack/spack/compilers/msvc.py
+++ b/lib/spack/spack/compilers/msvc.py
@@ -6,13 +6,17 @@
import os
import re
import subprocess
+import sys
from distutils.version import StrictVersion
from typing import Dict, List, Set # novm
+import spack.compiler
import spack.operating_systems.windows_os
+import spack.platforms
import spack.util.executable
from spack.compiler import Compiler
from spack.error import SpackError
+from spack.version import Version
avail_fc_version = set() # type: Set[str]
fc_path = dict() # type: Dict[str, str]
@@ -38,10 +42,10 @@ def get_valid_fortran_pth(comp_ver):
class Msvc(Compiler):
# Subclasses use possible names of C compiler
- cc_names = ["cl.exe"]
+ cc_names = ["cl.exe"] # type: List[str]
# Subclasses use possible names of C++ compiler
- cxx_names = ["cl.exe"]
+ cxx_names = ["cl.exe"] # type: List[str]
# Subclasses use possible names of Fortran 77 compiler
f77_names = ["ifx.exe"] # type: List[str]
@@ -90,10 +94,25 @@ class Msvc(Compiler):
@property
def msvc_version(self):
- ver = re.search(Msvc.version_regex, self.cc).group(1)
- ver = "".join(ver.split(".")[:2])[:-1]
+ """This is the VCToolset version *NOT* the actual version of the cl compiler
+ For CL version, query `Msvc.cl_version`"""
+ return Version(re.search(Msvc.version_regex, self.cc).group(1))
+
+ @property
+ def short_msvc_version(self):
+ """
+ This is the shorthand VCToolset version of form
+ MSVC<short-ver> *NOT* the full version, for that see
+ Msvc.msvc_version
+ """
+ ver = self.msvc_version[:2].joined.string[:3]
return "MSVC" + ver
+ @property
+ def cl_version(self):
+ """Cl toolset version"""
+ return spack.compiler.get_compiler_version_output(self.cc)
+
def setup_custom_environment(self, pkg, env):
"""Set environment variables for MSVC using the
Microsoft-provided script."""
@@ -103,11 +122,23 @@ class Msvc(Compiler):
# once the process terminates. So go the long way around: examine
# output, sort into dictionary, use that to make the build
# environment.
+
+ # get current platform architecture and format for vcvars argument
+ arch = spack.platforms.real_host().default.lower()
+ arch = arch.replace("-", "_")
+ # vcvars can target specific sdk versions, force it to pick up concretized sdk
+ # version, if needed by spec
+ sdk_ver = "" if "win-sdk" not in pkg.spec else pkg.spec["win-sdk"].version.string + ".0"
+ # provide vcvars with msvc version selected by concretization,
+ # not whatever it happens to pick up on the system (highest available version)
out = subprocess.check_output( # novermin
- 'cmd /u /c "{}" {} && set'.format(self.setvarsfile, "amd64"),
+ 'cmd /u /c "{}" {} {} {} && set'.format(
+ self.setvarsfile, arch, sdk_ver, "-vcvars_ver=%s" % self.msvc_version
+ ),
stderr=subprocess.STDOUT,
)
- out = out.decode("utf-16le", errors="replace") # novermin
+ if sys.version_info[0] >= 3:
+ out = out.decode("utf-16le", errors="replace") # novermin
int_env = dict(
(key.lower(), value)
diff --git a/lib/spack/spack/detection/common.py b/lib/spack/spack/detection/common.py
index 8233f47eb8..4e5bf0efcc 100644
--- a/lib/spack/spack/detection/common.py
+++ b/lib/spack/spack/detection/common.py
@@ -14,6 +14,7 @@ The module also contains other functions that might be useful across different
detection mechanisms.
"""
import collections
+import glob
import itertools
import os
import os.path
@@ -23,8 +24,10 @@ import sys
import llnl.util.tty
import spack.config
+import spack.operating_systems.windows_os as winOs
import spack.spec
import spack.util.spack_yaml
+import spack.util.windows_registry
is_windows = sys.platform == "win32"
#: Information on a package that has been detected
@@ -104,6 +107,19 @@ def _spec_is_valid(spec):
return True
+def path_to_dict(search_paths):
+ """Return dictionary[fullpath]: basename from list of paths"""
+ path_to_lib = {}
+ # Reverse order of search directories so that a lib in the first
+ # entry overrides later entries
+ for search_path in reversed(search_paths):
+ for lib in os.listdir(search_path):
+ lib_path = os.path.join(search_path, lib)
+ if llnl.util.filesystem.is_readable_file(lib_path):
+ path_to_lib[lib_path] = lib
+ return path_to_lib
+
+
def is_executable(file_path):
"""Return True if the path passed as argument is that of an executable"""
return os.path.isfile(file_path) and os.access(file_path, os.X_OK)
@@ -139,9 +155,11 @@ def executable_prefix(executable_dir):
assert os.path.isdir(executable_dir)
components = executable_dir.split(os.sep)
- if "bin" not in components:
+ # convert to lower to match Bin, BIN, bin
+ lowered_components = executable_dir.lower().split(os.sep)
+ if "bin" not in lowered_components:
return executable_dir
- idx = components.index("bin")
+ idx = lowered_components.index("bin")
return os.sep.join(components[:idx])
@@ -158,11 +176,16 @@ def library_prefix(library_dir):
assert os.path.isdir(library_dir)
components = library_dir.split(os.sep)
- if "lib64" in components:
- idx = components.index("lib64")
+ # covert to lowercase to match lib, LIB, Lib, etc.
+ lowered_components = library_dir.lower().split(os.sep)
+ if "lib64" in lowered_components:
+ idx = lowered_components.index("lib64")
return os.sep.join(components[:idx])
- elif "lib" in components:
- idx = components.index("lib")
+ elif "lib" in lowered_components:
+ idx = lowered_components.index("lib")
+ return os.sep.join(components[:idx])
+ elif is_windows and "bin" in lowered_components:
+ idx = lowered_components.index("bin")
return os.sep.join(components[:idx])
else:
return library_dir
@@ -195,10 +218,117 @@ def update_configuration(detected_packages, scope=None, buildable=True):
return all_new_specs
+def _windows_drive():
+ """Return Windows drive string"""
+ return os.environ["HOMEDRIVE"]
+
+
+class WindowsCompilerExternalPaths(object):
+ @staticmethod
+ def find_windows_compiler_root_paths():
+ """Helper for Windows compiler installation root discovery
+
+ At the moment simply returns location of VS install paths from VSWhere
+ But should be extended to include more information as relevant"""
+ return list(winOs.WindowsOs.vs_install_paths)
+
+ @staticmethod
+ def find_windows_compiler_cmake_paths():
+ """Semi hard-coded search path for cmake bundled with MSVC"""
+ return [
+ os.path.join(
+ path, "Common7", "IDE", "CommonExtensions", "Microsoft", "CMake", "CMake", "bin"
+ )
+ for path in WindowsCompilerExternalPaths.find_windows_compiler_root_paths()
+ ]
+
+ @staticmethod
+ def find_windows_compiler_ninja_paths():
+ """Semi hard-coded search heuristic for locating ninja bundled with MSVC"""
+ return [
+ os.path.join(path, "Common7", "IDE", "CommonExtensions", "Microsoft", "CMake", "Ninja")
+ for path in WindowsCompilerExternalPaths.find_windows_compiler_root_paths()
+ ]
+
+ @staticmethod
+ def find_windows_compiler_bundled_packages():
+ """Return all MSVC compiler bundled packages"""
+ return (
+ WindowsCompilerExternalPaths.find_windows_compiler_cmake_paths()
+ + WindowsCompilerExternalPaths.find_windows_compiler_ninja_paths()
+ )
+
+
+class WindowsKitExternalPaths(object):
+ if is_windows:
+ plat_major_ver = str(winOs.windows_version()[0])
+
+ @staticmethod
+ def find_windows_kit_roots():
+ """Return Windows kit root, typically %programfiles%\\Windows Kits\\10|11\\"""
+ if not is_windows:
+ return []
+ program_files = os.environ["PROGRAMFILES(x86)"]
+ kit_base = os.path.join(
+ program_files, "Windows Kits", WindowsKitExternalPaths.plat_major_ver
+ )
+ return kit_base
+
+ @staticmethod
+ def find_windows_kit_bin_paths(kit_base=None):
+ """Returns Windows kit bin directory per version"""
+ kit_base = WindowsKitExternalPaths.find_windows_kit_roots() if not kit_base else kit_base
+ kit_bin = os.path.join(kit_base, "bin")
+ return glob.glob(os.path.join(kit_bin, "[0-9]*", "*\\"))
+
+ @staticmethod
+ def find_windows_kit_lib_paths(kit_base=None):
+ """Returns Windows kit lib directory per version"""
+ kit_base = WindowsKitExternalPaths.find_windows_kit_roots() if not kit_base else kit_base
+ kit_lib = os.path.join(kit_base, "Lib")
+ return glob.glob(os.path.join(kit_lib, "[0-9]*", "*", "*\\"))
+
+ @staticmethod
+ def find_windows_driver_development_kit_paths():
+ """Provides a list of all installation paths
+ for the WDK by version and architecture
+ """
+ wdk_content_root = os.getenv("WDKContentRoot")
+ return WindowsKitExternalPaths.find_windows_kit_lib_paths(wdk_content_root)
+
+ @staticmethod
+ def find_windows_kit_reg_installed_roots_paths():
+ reg = spack.util.windows_registry.WindowsRegistryView(
+ "SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots",
+ root_key=spack.util.windows_registry.HKEY.HKEY_LOCAL_MACHINE,
+ )
+ if not reg:
+ # couldn't find key, return empty list
+ return []
+ return WindowsKitExternalPaths.find_windows_kit_lib_paths(
+ reg.get_value("KitsRoot%s" % WindowsKitExternalPaths.plat_major_ver).value
+ )
+
+ @staticmethod
+ def find_windows_kit_reg_sdk_paths():
+ reg = spack.util.windows_registry.WindowsRegistryView(
+ "SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v%s.0"
+ % WindowsKitExternalPaths.plat_major_ver,
+ root_key=spack.util.windows_registry.HKEY.HKEY_LOCAL_MACHINE,
+ )
+ if not reg:
+ # couldn't find key, return empty list
+ return []
+ return WindowsKitExternalPaths.find_windows_kit_lib_paths(
+ reg.get_value("InstallationFolder").value
+ )
+
+
def find_win32_additional_install_paths():
"""Not all programs on Windows live on the PATH
Return a list of other potential install locations.
"""
+ drive_letter = _windows_drive()
windows_search_ext = []
cuda_re = r"CUDA_PATH[a-zA-Z1-9_]*"
# The list below should be expanded with other
@@ -211,7 +341,7 @@ def find_win32_additional_install_paths():
# to interact with Windows
# Add search path for default Chocolatey (https://github.com/chocolatey/choco)
# install directory
- windows_search_ext.append("C:\\ProgramData\\chocolatey\\bin")
+ windows_search_ext.append("%s\\ProgramData\\chocolatey\\bin" % drive_letter)
# Add search path for NuGet package manager default install location
windows_search_ext.append(os.path.join(user, ".nuget", "packages"))
windows_search_ext.extend(
@@ -233,9 +363,32 @@ def compute_windows_program_path_for_package(pkg):
return []
# note windows paths are fine here as this method should only ever be invoked
# to interact with Windows
- program_files = "C:\\Program Files{}\\{}"
+ program_files = "{}\\Program Files{}\\{}"
+ drive_letter = _windows_drive()
return [
- program_files.format(arch, name)
+ program_files.format(drive_letter, arch, name)
for arch, name in itertools.product(("", " (x86)"), (pkg.name, pkg.name.capitalize()))
]
+
+
+def compute_windows_user_path_for_package(pkg):
+ """Given a package attempt to compute its user scoped
+ install location, return list of potential locations based
+ on common heuristics. For more info on Windows user specific
+ installs see:
+ https://learn.microsoft.com/en-us/dotnet/api/system.environment.specialfolder?view=netframework-4.8"""
+ if not is_windows:
+ return []
+
+ # Current user directory
+ user = os.environ["USERPROFILE"]
+ app_data = "AppData"
+ app_data_locations = ["Local", "Roaming"]
+ user_appdata_install_stubs = [os.path.join(app_data, x) for x in app_data_locations]
+ return [
+ os.path.join(user, app_data, name)
+ for app_data, name in list(
+ itertools.product(user_appdata_install_stubs, (pkg.name, pkg.name.capitalize()))
+ )
+ ] + [os.path.join(user, name) for name in (pkg.name, pkg.name.capitalize())]
diff --git a/lib/spack/spack/detection/path.py b/lib/spack/spack/detection/path.py
index bf42fa6144..ae297d86d2 100644
--- a/lib/spack/spack/detection/path.py
+++ b/lib/spack/spack/detection/path.py
@@ -15,22 +15,35 @@ import warnings
import llnl.util.filesystem
import llnl.util.tty
-import spack.operating_systems.windows_os as winOs
import spack.util.environment
import spack.util.ld_so_conf
-from .common import (
+from .common import ( # find_windows_compiler_bundled_packages,
DetectedPackage,
+ WindowsCompilerExternalPaths,
+ WindowsKitExternalPaths,
_convert_to_iterable,
compute_windows_program_path_for_package,
+ compute_windows_user_path_for_package,
executable_prefix,
find_win32_additional_install_paths,
- is_executable,
library_prefix,
+ path_to_dict,
)
+is_windows = sys.platform == "win32"
-def executables_in_path(path_hints=None):
+
+def common_windows_package_paths():
+ paths = WindowsCompilerExternalPaths.find_windows_compiler_bundled_packages()
+ paths.extend(find_win32_additional_install_paths())
+ paths.extend(WindowsKitExternalPaths.find_windows_kit_bin_paths())
+ paths.extend(WindowsKitExternalPaths.find_windows_kit_reg_installed_roots_paths())
+ paths.extend(WindowsKitExternalPaths.find_windows_kit_reg_sdk_paths())
+ return paths
+
+
+def executables_in_path(path_hints):
"""Get the paths of all executables available from the current PATH.
For convenience, this is constructed as a dictionary where the keys are
@@ -44,36 +57,10 @@ def executables_in_path(path_hints=None):
path_hints (list): list of paths to be searched. If None the list will be
constructed based on the PATH environment variable.
"""
- # If we're on a Windows box, run vswhere,
- # steal the installationPath using windows_os.py logic,
- # construct paths to CMake and Ninja, add to PATH
- path_hints = path_hints or spack.util.environment.get_path("PATH")
- if sys.platform == "win32":
- msvc_paths = list(winOs.WindowsOs.vs_install_paths)
- msvc_cmake_paths = [
- os.path.join(
- path, "Common7", "IDE", "CommonExtensions", "Microsoft", "CMake", "CMake", "bin"
- )
- for path in msvc_paths
- ]
- path_hints = msvc_cmake_paths + path_hints
- msvc_ninja_paths = [
- os.path.join(path, "Common7", "IDE", "CommonExtensions", "Microsoft", "CMake", "Ninja")
- for path in msvc_paths
- ]
- path_hints = msvc_ninja_paths + path_hints
- path_hints.extend(find_win32_additional_install_paths())
+ if is_windows:
+ path_hints.extend(common_windows_package_paths())
search_paths = llnl.util.filesystem.search_paths_for_executables(*path_hints)
-
- path_to_exe = {}
- # Reverse order of search directories so that an exe in the first PATH
- # entry overrides later entries
- for search_path in reversed(search_paths):
- for exe in os.listdir(search_path):
- exe_path = os.path.join(search_path, exe)
- if is_executable(exe_path):
- path_to_exe[exe_path] = exe
- return path_to_exe
+ return path_to_dict(search_paths)
def libraries_in_ld_and_system_library_path(path_hints=None):
@@ -102,16 +89,23 @@ def libraries_in_ld_and_system_library_path(path_hints=None):
+ spack.util.ld_so_conf.host_dynamic_linker_search_paths()
)
search_paths = llnl.util.filesystem.search_paths_for_libraries(*path_hints)
+ return path_to_dict(search_paths)
+
- path_to_lib = {}
- # Reverse order of search directories so that a lib in the first
- # LD_LIBRARY_PATH entry overrides later entries
- for search_path in reversed(search_paths):
- for lib in os.listdir(search_path):
- lib_path = os.path.join(search_path, lib)
- if llnl.util.filesystem.is_readable_file(lib_path):
- path_to_lib[lib_path] = lib
- return path_to_lib
+def libraries_in_windows_paths(path_hints):
+ path_hints.extend(spack.util.environment.get_path("PATH"))
+ search_paths = llnl.util.filesystem.search_paths_for_libraries(*path_hints)
+ # on Windows, some libraries (.dlls) are found in the bin directory or sometimes
+ # at the search root. Add both of those options to the search scheme
+ search_paths.extend(llnl.util.filesystem.search_paths_for_executables(*path_hints))
+ search_paths.extend(WindowsKitExternalPaths.find_windows_kit_lib_paths())
+ search_paths.extend(WindowsKitExternalPaths.find_windows_kit_bin_paths())
+ search_paths.extend(WindowsKitExternalPaths.find_windows_kit_reg_installed_roots_paths())
+ search_paths.extend(WindowsKitExternalPaths.find_windows_kit_reg_sdk_paths())
+ # SDK and WGL should be handled by above, however on occasion the WDK is in an atypical
+ # location, so we handle that case specifically.
+ search_paths.extend(WindowsKitExternalPaths.find_windows_driver_development_kit_paths())
+ return path_to_dict(search_paths)
def _group_by_prefix(paths):
@@ -141,12 +135,23 @@ def by_library(packages_to_check, path_hints=None):
DYLD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH environment variables
and standard system library paths.
"""
- path_to_lib_name = libraries_in_ld_and_system_library_path(path_hints=path_hints)
+ # If no path hints from command line, intialize to empty list so
+ # we can add default hints on a per package basis
+ path_hints = [] if path_hints is None else path_hints
+
lib_pattern_to_pkgs = collections.defaultdict(list)
for pkg in packages_to_check:
if hasattr(pkg, "libraries"):
for lib in pkg.libraries:
lib_pattern_to_pkgs[lib].append(pkg)
+ path_hints.extend(compute_windows_user_path_for_package(pkg))
+ path_hints.extend(compute_windows_program_path_for_package(pkg))
+
+ path_to_lib_name = (
+ libraries_in_ld_and_system_library_path(path_hints=path_hints)
+ if not is_windows
+ else libraries_in_windows_paths(path_hints)
+ )
pkg_to_found_libs = collections.defaultdict(set)
for lib_pattern, pkgs in lib_pattern_to_pkgs.items():
@@ -231,13 +236,14 @@ def by_executable(packages_to_check, path_hints=None):
path_hints (list): list of paths to be searched. If None the list will be
constructed based on the PATH environment variable.
"""
- path_hints = [] if path_hints is None else path_hints
+ path_hints = spack.util.environment.get_path("PATH") if path_hints is None else path_hints
exe_pattern_to_pkgs = collections.defaultdict(list)
for pkg in packages_to_check:
if hasattr(pkg, "executables"):
for exe in pkg.platform_executables():
exe_pattern_to_pkgs[exe].append(pkg)
# Add Windows specific, package related paths to the search paths
+ path_hints.extend(compute_windows_user_path_for_package(pkg))
path_hints.extend(compute_windows_program_path_for_package(pkg))
path_to_exe_name = executables_in_path(path_hints=path_hints)
diff --git a/lib/spack/spack/operating_systems/windows_os.py b/lib/spack/spack/operating_systems/windows_os.py
index ec563f5336..312a845d19 100755
--- a/lib/spack/spack/operating_systems/windows_os.py
+++ b/lib/spack/spack/operating_systems/windows_os.py
@@ -1,4 +1,4 @@
-# Copyright 2013-2021 Lawrence Livermore National Security, LLC and other
+# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
@@ -15,8 +15,12 @@ from ._operating_system import OperatingSystem
def windows_version():
- """temporary workaround to return a Windows version as a Version object"""
- return Version(platform.release())
+ """Windows version as a Version object"""
+ # include the build number as this provides important information
+ # for low lever packages and components like the SDK and WDK
+ # The build number is the version component that would otherwise
+ # be the patch version in sematic versioning, i.e. z of x.y.z
+ return Version(platform.version())
class WindowsOs(OperatingSystem):
@@ -65,8 +69,8 @@ class WindowsOs(OperatingSystem):
compiler_search_paths = comp_search_paths
def __init__(self):
- plat_ver = platform.release()
- if Version(plat_ver) < Version("10"):
+ plat_ver = windows_version()
+ if plat_ver < Version("10"):
raise SpackError("Spack is not supported on Windows versions older than 10")
super(WindowsOs, self).__init__("windows{}".format(plat_ver), plat_ver)
diff --git a/lib/spack/spack/test/cmd/external.py b/lib/spack/spack/test/cmd/external.py
index ec9923139c..1944a2e940 100644
--- a/lib/spack/spack/test/cmd/external.py
+++ b/lib/spack/spack/test/cmd/external.py
@@ -107,9 +107,7 @@ def test_find_external_update_config(mutable_config):
def test_get_executables(working_env, mock_executable):
cmake_path1 = mock_executable("cmake", output="echo cmake version 1.foo")
-
- os.environ["PATH"] = os.pathsep.join([os.path.dirname(cmake_path1)])
- path_to_exe = spack.detection.executables_in_path()
+ path_to_exe = spack.detection.executables_in_path([os.path.dirname(cmake_path1)])
cmake_exe = define_plat_exe("cmake")
assert path_to_exe[cmake_path1] == cmake_exe
diff --git a/lib/spack/spack/util/windows_registry.py b/lib/spack/spack/util/windows_registry.py
new file mode 100644
index 0000000000..bdf84b5b95
--- /dev/null
+++ b/lib/spack/spack/util/windows_registry.py
@@ -0,0 +1,291 @@
+# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+"""
+Utility module for dealing with Windows Registry.
+"""
+
+import os
+import sys
+from contextlib import contextmanager
+
+from llnl.util import tty
+
+is_windows = sys.platform == "win32"
+if is_windows:
+ import winreg
+
+
+class RegistryValue(object):
+ """
+ Class defining a Windows registry entry
+ """
+
+ def __init__(self, name, value, parent_key):
+ self.path = name
+ self.value = value
+ self.key = parent_key
+
+
+class RegistryKey(object):
+ """
+ Class wrapping a Windows registry key
+ """
+
+ def __init__(self, name, handle):
+ self.path = name
+ self.name = os.path.split(name)[-1]
+ self._handle = handle
+ self._keys = []
+ self._values = {}
+
+ @property
+ def values(self):
+ """Returns all subvalues of this key as RegistryValue objects in dictionary
+ of value name : RegistryValue object
+ """
+ self._gather_value_info()
+ return self._values
+
+ @property
+ def subkeys(self):
+ """Returns list of all subkeys of this key as RegistryKey objects"""
+ self._gather_subkey_info()
+ return self._keys
+
+ @property
+ def hkey(self):
+ return self._handle
+
+ def __str__(self):
+ return self.name
+
+ def _gather_subkey_info(self):
+ """Composes all subkeys into a list for access"""
+ if self._keys:
+ return
+ sub_keys, _, _ = winreg.QueryInfoKey(self.hkey)
+ for i in range(sub_keys):
+ sub_name = winreg.EnumKey(self.hkey, i)
+ 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))
+
+ def _gather_value_info(self):
+ """Compose all values for this key into a dict of form value name: RegistryValue Object"""
+ if self._values:
+ return
+ _, values, _ = winreg.QueryInfoKey(self.hkey)
+ for i in range(values):
+ value_name, value_data, _ = winreg.EnumValue(self.hkey, i)
+ self._values[value_name] = RegistryValue(value_name, value_data, self.hkey)
+
+ def get_subkey(self, sub_key):
+ """Returns subkey of name sub_key in a RegistryKey objects"""
+ return RegistryKey(
+ os.path.join(self.path, sub_key),
+ winreg.OpenKeyEx(self.hkey, sub_key, access=winreg.KEY_READ),
+ )
+
+ def get_value(self, val_name):
+ """Returns value associated with this key in RegistryValue object"""
+ return RegistryValue(val_name, winreg.QueryValueEx(self.hkey, val_name)[0], self.hkey)
+
+
+class _HKEY_CONSTANT(RegistryKey):
+ """Subclass of RegistryKey to represent the prebaked, always open registry HKEY constants"""
+
+ def __init__(self, hkey_constant):
+ hkey_name = hkey_constant
+ # This class is instantiated at module import time
+ # on non Windows platforms, winreg would not have been
+ # imported. For this reason we can't reference winreg yet,
+ # so handle is none for now to avoid invalid references to a module.
+ # _handle provides a workaround to prevent null references to self.handle
+ # when coupled with the handle property.
+ super(_HKEY_CONSTANT, self).__init__(hkey_name, None)
+
+ def _get_hkey(self, key):
+ return getattr(winreg, key)
+
+ @property
+ def hkey(self):
+ if not self._handle:
+ self._handle = self._get_hkey(self.path)
+ return self._handle
+
+
+class HKEY(object):
+ """
+ Predefined, open registry HKEYs
+ From the Microsoft docs:
+ An application must open a key before it can read data from the registry.
+ To open a key, an application must supply a handle to another key in
+ the registry that is already open. The system defines predefined keys
+ that are always open. Predefined keys help an application navigate in
+ the registry."""
+
+ HKEY_CLASSES_ROOT = _HKEY_CONSTANT("HKEY_CLASSES_ROOT")
+ HKEY_CURRENT_USER = _HKEY_CONSTANT("HKEY_CURRENT_USER")
+ HKEY_USERS = _HKEY_CONSTANT("HKEY_USERS")
+ HKEY_LOCAL_MACHINE = _HKEY_CONSTANT("HKEY_LOCAL_MACHINE")
+ HKEY_CURRENT_CONFIG = _HKEY_CONSTANT("HKEY_CURRENT_CONFIG")
+ HKEY_PERFORMANCE_DATA = _HKEY_CONSTANT("HKEY_PERFORMANCE_DATA")
+
+
+class WindowsRegistryView(object):
+ """
+ Interface to provide access, querying, and searching to Windows registry entries.
+ This class represents a single key entrypoint into the Windows registry
+ and provides an interface to this key's values, its subkeys, and those subkey's values.
+ This class cannot be used to move freely about the registry, only subkeys/values of
+ the root key used to instantiate this class.
+ """
+
+ def __init__(self, key, root_key=HKEY.HKEY_CURRENT_USER):
+ """Constructs a Windows Registry entrypoint to key provided
+ root_key should be an already open root key or an hkey constant if provided
+
+ Args:
+ key (str): registry key to provide root for registry key for this clas
+ root_key: Already open registry key or HKEY constant to provide access into
+ the Windows registry. Registry access requires an already open key
+ to get an entrypoint, the HKEY constants are always open, or an already
+ open key can be used instead.
+ """
+ if not is_windows:
+ raise RuntimeError(
+ "Cannot instantiate Windows Registry class on non Windows platforms"
+ )
+ self.key = key
+ self.root = root_key
+ self._reg = None
+
+ @contextmanager
+ def invalid_reg_ref_error_handler(self):
+ try:
+ yield
+ except FileNotFoundError as e:
+ if e.winerror == 2:
+ tty.debug("Key %s at position %s does not exist" % (self.key, str(self.root)))
+ else:
+ raise e
+
+ def __bool__(self):
+ return self.reg != -1
+
+ def _load_key(self):
+ try:
+ self._reg = RegistryKey(
+ os.path.join(str(self.root), self.key),
+ winreg.OpenKeyEx(self.root.hkey, self.key, access=winreg.KEY_READ),
+ )
+ except FileNotFoundError as e:
+ if e.winerror == 2:
+ self._reg = -1
+ tty.debug("Key %s at position %s does not exist" % (self.key, str(self.root)))
+ else:
+ raise e
+
+ def _valid_reg_check(self):
+ if self.reg == -1:
+ tty.debug("Cannot perform operation for nonexistent key %s" % self.key)
+ return False
+ return True
+
+ @property
+ def reg(self):
+ if not self._reg:
+ self._load_key()
+ return self._reg
+
+ def get_value(self, value_name):
+ """Return registry value corresponding to provided argument (if it exists)"""
+ if not self._valid_reg_check():
+ raise RegistryError("Cannot query value from invalid key %s" % self.key)
+ with self.invalid_reg_ref_error_handler():
+ return self.reg.get_value(value_name)
+
+ def get_subkey(self, subkey_name):
+ if not self._valid_reg_check():
+ raise RegistryError("Cannot query subkey from invalid key %s" % self.key)
+ with self.invalid_reg_ref_error_handler():
+ return self.reg.get_subkey(subkey_name)
+
+ def get_subkeys(self):
+ if not self._valid_reg_check():
+ raise RegistryError("Cannot query subkeys from invalid key %s" % self.key)
+ with self.invalid_reg_ref_error_handler():
+ return self.reg.subkeys
+
+ def get_values(self):
+ if not self._valid_reg_check():
+ raise RegistryError("Cannot query values from invalid key %s" % self.key)
+ with self.invalid_reg_ref_error_handler():
+ return self.reg.values
+
+ def _traverse_subkeys(self, stop_condition):
+ """Perform simple BFS of subkeys, returning the key
+ that successfully triggers the stop condition.
+ Args:
+ stop_condition: lambda or function pointer that takes a single argument
+ a key and returns a boolean value based on that key
+ Return:
+ the key if stop_condition is triggered, or None if not
+ """
+ if not self._valid_reg_check():
+ raise RegistryError("Cannot query values from invalid key %s" % self.key)
+ with self.invalid_reg_ref_error_handler():
+ queue = self.reg.subkeys
+ for key in queue:
+ if stop_condition(key):
+ return key
+ queue.extend(key.subkeys)
+ return None
+
+ def find_subkey(self, subkey_name, recursive=True):
+ """If non recursive, this method is the same as get subkey with error handling
+ Otherwise perform a BFS of subkeys until desired key is found
+ Returns None or RegistryKey object corresponding to requested key name
+
+ Args:
+ subkey_name (str): string representing subkey to be searched for
+ recursive (bool): optional argument, if True, subkey need not be a direct
+ sub key of this registry entry, and this method will
+ search all subkeys recursively.
+ Default is True
+ Return:
+ the desired subkey as a RegistryKey object, or none
+ """
+
+ if not recursive:
+ return self.get_subkey(subkey_name)
+
+ else:
+ return self._traverse_subkeys(lambda x: x.name == subkey_name)
+
+ def find_value(self, val_name, recursive=True):
+ """
+ If non recursive, return RegistryValue object corresponding to name
+
+ Args:
+ val_name (str): name of value desired from registry
+ recursive (bool): optional argument, if True, the registry is searched recursively
+ for the value of name val_name, else only the current key is searched
+ Return:
+ The desired registry value as a RegistryValue object if it exists, otherwise, None
+ """
+ if not recursive:
+ return self.get_value(val_name)
+
+ else:
+ key = self._traverse_subkeys(lambda x: val_name in x.values)
+ if not key:
+ return None
+ else:
+ return key.values[val_name]
+
+
+class RegistryError(RuntimeError):
+ """Runtime Error describing issue with invalid key access to Windows registry"""