summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
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"""