Add type-hints to spack.bootstrap (#36491)

This commit is contained in:
Massimiliano Culpo 2023-03-30 22:12:18 +02:00 committed by GitHub
parent f5624f096c
commit e1a104e3a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 117 additions and 78 deletions

View file

@ -9,6 +9,7 @@
import sys
import sysconfig
import warnings
from typing import Dict, Optional, Sequence, Union
import archspec.cpu
@ -21,8 +22,10 @@
from .config import spec_for_current_python
QueryInfo = Dict[str, "spack.spec.Spec"]
def _python_import(module):
def _python_import(module: str) -> bool:
try:
__import__(module)
except ImportError:
@ -30,7 +33,9 @@ def _python_import(module):
return True
def _try_import_from_store(module, query_spec, query_info=None):
def _try_import_from_store(
module: str, query_spec: Union[str, "spack.spec.Spec"], query_info: Optional[QueryInfo] = None
) -> bool:
"""Return True if the module can be imported from an already
installed spec, False otherwise.
@ -52,7 +57,7 @@ def _try_import_from_store(module, query_spec, query_info=None):
module_paths = [
os.path.join(candidate_spec.prefix, pkg.purelib),
os.path.join(candidate_spec.prefix, pkg.platlib),
] # type: list[str]
]
path_before = list(sys.path)
# NOTE: try module_paths first and last, last allows an existing version in path
@ -89,7 +94,7 @@ def _try_import_from_store(module, query_spec, query_info=None):
return False
def _fix_ext_suffix(candidate_spec):
def _fix_ext_suffix(candidate_spec: "spack.spec.Spec"):
"""Fix the external suffixes of Python extensions on the fly for
platforms that may need it
@ -157,7 +162,11 @@ def _fix_ext_suffix(candidate_spec):
os.symlink(abs_path, link_name)
def _executables_in_store(executables, query_spec, query_info=None):
def _executables_in_store(
executables: Sequence[str],
query_spec: Union["spack.spec.Spec", str],
query_info: Optional[QueryInfo] = None,
) -> bool:
"""Return True if at least one of the executables can be retrieved from
a spec in store, False otherwise.
@ -193,7 +202,7 @@ def _executables_in_store(executables, query_spec, query_info=None):
return False
def _root_spec(spec_str):
def _root_spec(spec_str: str) -> str:
"""Add a proper compiler and target to a spec used during bootstrapping.
Args:

View file

@ -7,6 +7,7 @@
import contextlib
import os.path
import sys
from typing import Any, Dict, Generator, MutableSequence, Sequence
from llnl.util import tty
@ -24,12 +25,12 @@
_REF_COUNT = 0
def is_bootstrapping():
def is_bootstrapping() -> bool:
"""Return True if we are in a bootstrapping context, False otherwise."""
return _REF_COUNT > 0
def spec_for_current_python():
def spec_for_current_python() -> str:
"""For bootstrapping purposes we are just interested in the Python
minor version (all patches are ABI compatible with the same minor).
@ -41,14 +42,14 @@ def spec_for_current_python():
return f"python@{version_str}"
def root_path():
def root_path() -> str:
"""Root of all the bootstrap related folders"""
return spack.util.path.canonicalize_path(
spack.config.get("bootstrap:root", spack.paths.default_user_bootstrap_path)
)
def store_path():
def store_path() -> str:
"""Path to the store used for bootstrapped software"""
enabled = spack.config.get("bootstrap:enable", True)
if not enabled:
@ -59,7 +60,7 @@ def store_path():
@contextlib.contextmanager
def spack_python_interpreter():
def spack_python_interpreter() -> Generator:
"""Override the current configuration to set the interpreter under
which Spack is currently running as the only Python external spec
available.
@ -76,18 +77,18 @@ def spack_python_interpreter():
yield
def _store_path():
def _store_path() -> str:
bootstrap_root_path = root_path()
return spack.util.path.canonicalize_path(os.path.join(bootstrap_root_path, "store"))
def _config_path():
def _config_path() -> str:
bootstrap_root_path = root_path()
return spack.util.path.canonicalize_path(os.path.join(bootstrap_root_path, "config"))
@contextlib.contextmanager
def ensure_bootstrap_configuration():
def ensure_bootstrap_configuration() -> Generator:
"""Swap the current configuration for the one used to bootstrap Spack.
The context manager is reference counted to ensure we don't swap multiple
@ -107,7 +108,7 @@ def ensure_bootstrap_configuration():
_REF_COUNT -= 1
def _read_and_sanitize_configuration():
def _read_and_sanitize_configuration() -> Dict[str, Any]:
"""Read the user configuration that needs to be reused for bootstrapping
and remove the entries that should not be copied over.
"""
@ -120,9 +121,11 @@ def _read_and_sanitize_configuration():
return user_configuration
def _bootstrap_config_scopes():
def _bootstrap_config_scopes() -> Sequence["spack.config.ConfigScope"]:
tty.debug("[BOOTSTRAP CONFIG SCOPE] name=_builtin")
config_scopes = [spack.config.InternalConfigScope("_builtin", spack.config.config_defaults)]
config_scopes: MutableSequence["spack.config.ConfigScope"] = [
spack.config.InternalConfigScope("_builtin", spack.config.config_defaults)
]
configuration_paths = (spack.config.configuration_defaults_path, ("bootstrap", _config_path()))
for name, path in configuration_paths:
platform = spack.platforms.host().name
@ -137,7 +140,7 @@ def _bootstrap_config_scopes():
return config_scopes
def _add_compilers_if_missing():
def _add_compilers_if_missing() -> None:
arch = spack.spec.ArchSpec.frontend_arch()
if not spack.compilers.compilers_for_arch(arch):
new_compilers = spack.compilers.find_new_compilers()
@ -146,7 +149,7 @@ def _add_compilers_if_missing():
@contextlib.contextmanager
def _ensure_bootstrap_configuration():
def _ensure_bootstrap_configuration() -> Generator:
bootstrap_store_path = store_path()
user_configuration = _read_and_sanitize_configuration()
with spack.environment.no_active_environment():

View file

@ -29,7 +29,7 @@
import os.path
import sys
import uuid
from typing import Callable, List, Optional
from typing import Any, Callable, Dict, List, Optional, Tuple
from llnl.util import tty
from llnl.util.lang import GroupedExceptionHandler
@ -66,6 +66,9 @@
_bootstrap_methods = {}
ConfigDictionary = Dict[str, Any]
def bootstrapper(bootstrapper_type: str):
"""Decorator to register classes implementing bootstrapping
methods.
@ -86,7 +89,7 @@ class Bootstrapper:
config_scope_name = ""
def __init__(self, conf):
def __init__(self, conf: ConfigDictionary) -> None:
self.conf = conf
self.name = conf["name"]
self.metadata_dir = spack.util.path.canonicalize_path(conf["metadata"])
@ -100,7 +103,7 @@ def __init__(self, conf):
self.url = url
@property
def mirror_scope(self):
def mirror_scope(self) -> spack.config.InternalConfigScope:
"""Mirror scope to be pushed onto the bootstrapping configuration when using
this bootstrapper.
"""
@ -121,7 +124,7 @@ def try_import(self, module: str, abstract_spec_str: str) -> bool:
"""
return False
def try_search_path(self, executables: List[str], abstract_spec_str: str) -> bool:
def try_search_path(self, executables: Tuple[str], abstract_spec_str: str) -> bool:
"""Try to search some executables in the prefix of specs satisfying the abstract
spec passed as argument.
@ -139,13 +142,15 @@ def try_search_path(self, executables: List[str], abstract_spec_str: str) -> boo
class BuildcacheBootstrapper(Bootstrapper):
"""Install the software needed during bootstrapping from a buildcache."""
def __init__(self, conf):
def __init__(self, conf) -> None:
super().__init__(conf)
self.last_search = None
self.last_search: Optional[ConfigDictionary] = None
self.config_scope_name = f"bootstrap_buildcache-{uuid.uuid4()}"
@staticmethod
def _spec_and_platform(abstract_spec_str):
def _spec_and_platform(
abstract_spec_str: str,
) -> Tuple[spack.spec.Spec, spack.platforms.Platform]:
"""Return the spec object and platform we need to use when
querying the buildcache.
@ -158,7 +163,7 @@ def _spec_and_platform(abstract_spec_str):
bincache_platform = spack.platforms.real_host()
return abstract_spec, bincache_platform
def _read_metadata(self, package_name):
def _read_metadata(self, package_name: str) -> Any:
"""Return metadata about the given package."""
json_filename = f"{package_name}.json"
json_dir = self.metadata_dir
@ -167,7 +172,13 @@ def _read_metadata(self, package_name):
data = json.load(stream)
return data
def _install_by_hash(self, pkg_hash, pkg_sha256, index, bincache_platform):
def _install_by_hash(
self,
pkg_hash: str,
pkg_sha256: str,
index: List[spack.spec.Spec],
bincache_platform: spack.platforms.Platform,
) -> None:
index_spec = next(x for x in index if x.dag_hash() == pkg_hash)
# Reconstruct the compiler that we need to use for bootstrapping
compiler_entry = {
@ -192,7 +203,13 @@ def _install_by_hash(self, pkg_hash, pkg_sha256, index, bincache_platform):
match, allow_root=True, unsigned=True, force=True, sha256=pkg_sha256
)
def _install_and_test(self, abstract_spec, bincache_platform, bincache_data, test_fn):
def _install_and_test(
self,
abstract_spec: spack.spec.Spec,
bincache_platform: spack.platforms.Platform,
bincache_data,
test_fn,
) -> bool:
# Ensure we see only the buildcache being used to bootstrap
with spack.config.override(self.mirror_scope):
# This index is currently needed to get the compiler used to build some
@ -217,13 +234,14 @@ def _install_and_test(self, abstract_spec, bincache_platform, bincache_data, tes
for _, pkg_hash, pkg_sha256 in item["binaries"]:
self._install_by_hash(pkg_hash, pkg_sha256, index, bincache_platform)
info = {}
info: ConfigDictionary = {}
if test_fn(query_spec=abstract_spec, query_info=info):
self.last_search = info
return True
return False
def try_import(self, module, abstract_spec_str):
def try_import(self, module: str, abstract_spec_str: str) -> bool:
info: ConfigDictionary
test_fn, info = functools.partial(_try_import_from_store, module), {}
if test_fn(query_spec=abstract_spec_str, query_info=info):
return True
@ -235,7 +253,8 @@ def try_import(self, module, abstract_spec_str):
data = self._read_metadata(module)
return self._install_and_test(abstract_spec, bincache_platform, data, test_fn)
def try_search_path(self, executables, abstract_spec_str):
def try_search_path(self, executables: Tuple[str], abstract_spec_str: str) -> bool:
info: ConfigDictionary
test_fn, info = functools.partial(_executables_in_store, executables), {}
if test_fn(query_spec=abstract_spec_str, query_info=info):
self.last_search = info
@ -251,13 +270,13 @@ def try_search_path(self, executables, abstract_spec_str):
class SourceBootstrapper(Bootstrapper):
"""Install the software needed during bootstrapping from sources."""
def __init__(self, conf):
def __init__(self, conf) -> None:
super().__init__(conf)
self.last_search = None
self.last_search: Optional[ConfigDictionary] = None
self.config_scope_name = f"bootstrap_source-{uuid.uuid4()}"
def try_import(self, module, abstract_spec_str):
info = {}
def try_import(self, module: str, abstract_spec_str: str) -> bool:
info: ConfigDictionary = {}
if _try_import_from_store(module, abstract_spec_str, query_info=info):
self.last_search = info
return True
@ -293,8 +312,8 @@ def try_import(self, module, abstract_spec_str):
return True
return False
def try_search_path(self, executables, abstract_spec_str):
info = {}
def try_search_path(self, executables: Tuple[str], abstract_spec_str: str) -> bool:
info: ConfigDictionary = {}
if _executables_in_store(executables, abstract_spec_str, query_info=info):
self.last_search = info
return True
@ -323,13 +342,13 @@ def try_search_path(self, executables, abstract_spec_str):
return False
def create_bootstrapper(conf):
def create_bootstrapper(conf: ConfigDictionary):
"""Return a bootstrap object built according to the configuration argument"""
btype = conf["type"]
return _bootstrap_methods[btype](conf)
def source_is_enabled_or_raise(conf):
def source_is_enabled_or_raise(conf: ConfigDictionary):
"""Raise ValueError if the source is not enabled for bootstrapping"""
trusted, name = spack.config.get("bootstrap:trusted"), conf["name"]
if not trusted.get(name, False):
@ -454,7 +473,7 @@ def ensure_executables_in_path_or_raise(
raise RuntimeError(msg)
def _add_externals_if_missing():
def _add_externals_if_missing() -> None:
search_list = [
# clingo
spack.repo.path.get_pkg_class("cmake"),
@ -468,41 +487,41 @@ def _add_externals_if_missing():
spack.detection.update_configuration(detected_packages, scope="bootstrap")
def clingo_root_spec():
def clingo_root_spec() -> str:
"""Return the root spec used to bootstrap clingo"""
return _root_spec("clingo-bootstrap@spack+python")
def ensure_clingo_importable_or_raise():
def ensure_clingo_importable_or_raise() -> None:
"""Ensure that the clingo module is available for import."""
ensure_module_importable_or_raise(module="clingo", abstract_spec=clingo_root_spec())
def gnupg_root_spec():
def gnupg_root_spec() -> str:
"""Return the root spec used to bootstrap GnuPG"""
return _root_spec("gnupg@2.3:")
def ensure_gpg_in_path_or_raise():
def ensure_gpg_in_path_or_raise() -> None:
"""Ensure gpg or gpg2 are in the PATH or raise."""
return ensure_executables_in_path_or_raise(
executables=["gpg2", "gpg"], abstract_spec=gnupg_root_spec()
)
def patchelf_root_spec():
def patchelf_root_spec() -> str:
"""Return the root spec used to bootstrap patchelf"""
# 0.13.1 is the last version not to require C++17.
return _root_spec("patchelf@0.13.1:")
def verify_patchelf(patchelf):
def verify_patchelf(patchelf: "spack.util.executable.Executable") -> bool:
"""Older patchelf versions can produce broken binaries, so we
verify the version here.
Arguments:
patchelf (spack.util.executable.Executable): patchelf executable
patchelf: patchelf executable
"""
out = patchelf("--version", output=str, error=os.devnull, fail_on_error=False).strip()
if patchelf.returncode != 0:
@ -517,7 +536,7 @@ def verify_patchelf(patchelf):
return version >= spack.version.Version("0.13.1")
def ensure_patchelf_in_path_or_raise():
def ensure_patchelf_in_path_or_raise() -> None:
"""Ensure patchelf is in the PATH or raise."""
# The old concretizer is not smart and we're doing its job: if the latest patchelf
# does not concretize because the compiler doesn't support C++17, we try to
@ -534,7 +553,7 @@ def ensure_patchelf_in_path_or_raise():
)
def ensure_core_dependencies():
def ensure_core_dependencies() -> None:
"""Ensure the presence of all the core dependencies."""
if sys.platform.lower() == "linux":
ensure_patchelf_in_path_or_raise()
@ -543,7 +562,7 @@ def ensure_core_dependencies():
ensure_clingo_importable_or_raise()
def all_core_root_specs():
def all_core_root_specs() -> List[str]:
"""Return a list of all the core root specs that may be used to bootstrap Spack"""
return [clingo_root_spec(), gnupg_root_spec(), patchelf_root_spec()]

View file

@ -9,6 +9,7 @@
import pathlib
import sys
import warnings
from typing import List
import archspec.cpu
@ -27,7 +28,7 @@ class BootstrapEnvironment(spack.environment.Environment):
"""Environment to install dependencies of Spack for a given interpreter and architecture"""
@classmethod
def spack_dev_requirements(cls):
def spack_dev_requirements(cls) -> List[str]:
"""Spack development requirements"""
return [
isort_root_spec(),
@ -38,7 +39,7 @@ def spack_dev_requirements(cls):
]
@classmethod
def environment_root(cls):
def environment_root(cls) -> pathlib.Path:
"""Environment root directory"""
bootstrap_root_path = root_path()
python_part = spec_for_current_python().replace("@", "")
@ -52,12 +53,12 @@ def environment_root(cls):
)
@classmethod
def view_root(cls):
def view_root(cls) -> pathlib.Path:
"""Location of the view"""
return cls.environment_root().joinpath("view")
@classmethod
def pythonpaths(cls):
def pythonpaths(cls) -> List[str]:
"""Paths to be added to sys.path or PYTHONPATH"""
python_dir_part = f"python{'.'.join(str(x) for x in sys.version_info[:2])}"
glob_expr = str(cls.view_root().joinpath("**", python_dir_part, "**"))
@ -68,21 +69,21 @@ def pythonpaths(cls):
return result
@classmethod
def bin_dirs(cls):
def bin_dirs(cls) -> List[pathlib.Path]:
"""Paths to be added to PATH"""
return [cls.view_root().joinpath("bin")]
@classmethod
def spack_yaml(cls):
def spack_yaml(cls) -> pathlib.Path:
"""Environment spack.yaml file"""
return cls.environment_root().joinpath("spack.yaml")
def __init__(self):
def __init__(self) -> None:
if not self.spack_yaml().exists():
self._write_spack_yaml_file()
super().__init__(self.environment_root())
def update_installations(self):
def update_installations(self) -> None:
"""Update the installations of this environment.
The update is done using a depfile on Linux and macOS, and using the ``install_all``
@ -103,7 +104,7 @@ def update_installations(self):
self._install_with_depfile()
self.write(regenerate=True)
def update_syspath_and_environ(self):
def update_syspath_and_environ(self) -> None:
"""Update ``sys.path`` and the PATH, PYTHONPATH environment variables to point to
the environment view.
"""
@ -119,7 +120,7 @@ def update_syspath_and_environ(self):
+ [str(x) for x in self.pythonpaths()]
)
def _install_with_depfile(self):
def _install_with_depfile(self) -> None:
spackcmd = spack.util.executable.which("spack")
spackcmd(
"-e",
@ -141,7 +142,7 @@ def _install_with_depfile(self):
**kwargs,
)
def _write_spack_yaml_file(self):
def _write_spack_yaml_file(self) -> None:
tty.msg(
"[BOOTSTRAPPING] Spack has missing dependencies, creating a bootstrapping environment"
)
@ -159,32 +160,32 @@ def _write_spack_yaml_file(self):
self.spack_yaml().write_text(template.render(context), encoding="utf-8")
def isort_root_spec():
def isort_root_spec() -> str:
"""Return the root spec used to bootstrap isort"""
return _root_spec("py-isort@4.3.5:")
def mypy_root_spec():
def mypy_root_spec() -> str:
"""Return the root spec used to bootstrap mypy"""
return _root_spec("py-mypy@0.900:")
def black_root_spec():
def black_root_spec() -> str:
"""Return the root spec used to bootstrap black"""
return _root_spec("py-black@:23.1.0")
def flake8_root_spec():
def flake8_root_spec() -> str:
"""Return the root spec used to bootstrap flake8"""
return _root_spec("py-flake8")
def pytest_root_spec():
def pytest_root_spec() -> str:
"""Return the root spec used to bootstrap flake8"""
return _root_spec("py-pytest")
def ensure_environment_dependencies():
def ensure_environment_dependencies() -> None:
"""Ensure Spack dependencies from the bootstrap environment are installed and ready to use"""
with BootstrapEnvironment() as env:
env.update_installations()

View file

@ -4,6 +4,7 @@
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Query the status of bootstrapping on this machine"""
import platform
from typing import List, Optional, Sequence, Tuple, Union
import spack.util.executable
@ -19,8 +20,12 @@
pytest_root_spec,
)
ExecutablesType = Union[str, Sequence[str]]
RequiredResponseType = Tuple[bool, Optional[str]]
SpecLike = Union["spack.spec.Spec", str]
def _required_system_executable(exes, msg):
def _required_system_executable(exes: ExecutablesType, msg: str) -> RequiredResponseType:
"""Search for an executable is the system path only."""
if isinstance(exes, str):
exes = (exes,)
@ -29,7 +34,9 @@ def _required_system_executable(exes, msg):
return False, msg
def _required_executable(exes, query_spec, msg):
def _required_executable(
exes: ExecutablesType, query_spec: SpecLike, msg: str
) -> RequiredResponseType:
"""Search for an executable in the system path or in the bootstrap store."""
if isinstance(exes, str):
exes = (exes,)
@ -38,7 +45,7 @@ def _required_executable(exes, query_spec, msg):
return False, msg
def _required_python_module(module, query_spec, msg):
def _required_python_module(module: str, query_spec: SpecLike, msg: str) -> RequiredResponseType:
"""Check if a Python module is available in the current interpreter or
if it can be loaded from the bootstrap store
"""
@ -47,7 +54,7 @@ def _required_python_module(module, query_spec, msg):
return False, msg
def _missing(name, purpose, system_only=True):
def _missing(name: str, purpose: str, system_only: bool = True) -> str:
"""Message to be printed if an executable is not found"""
msg = '[{2}] MISSING "{0}": {1}'
if not system_only:
@ -55,7 +62,7 @@ def _missing(name, purpose, system_only=True):
return msg.format(name, purpose, "@*y{{-}}")
def _core_requirements():
def _core_requirements() -> List[RequiredResponseType]:
_core_system_exes = {
"make": _missing("make", "required to build software from sources"),
"patch": _missing("patch", "required to patch source code before building"),
@ -80,7 +87,7 @@ def _core_requirements():
return result
def _buildcache_requirements():
def _buildcache_requirements() -> List[RequiredResponseType]:
_buildcache_exes = {
"file": _missing("file", "required to analyze files for buildcaches"),
("gpg2", "gpg"): _missing("gpg2", "required to sign/verify buildcaches", False),
@ -103,7 +110,7 @@ def _buildcache_requirements():
return result
def _optional_requirements():
def _optional_requirements() -> List[RequiredResponseType]:
_optional_exes = {
"zstd": _missing("zstd", "required to compress/decompress code archives"),
"svn": _missing("svn", "required to manage subversion repositories"),
@ -114,7 +121,7 @@ def _optional_requirements():
return result
def _development_requirements():
def _development_requirements() -> List[RequiredResponseType]:
# Ensure we trigger environment modifications if we have an environment
if BootstrapEnvironment.spack_yaml().exists():
with BootstrapEnvironment() as env:
@ -139,7 +146,7 @@ def _development_requirements():
]
def status_message(section):
def status_message(section) -> Tuple[str, bool]:
"""Return a status message to be printed to screen that refers to the
section passed as argument and a bool which is True if there are missing
dependencies.
@ -161,7 +168,7 @@ def status_message(section):
with ensure_bootstrap_configuration():
missing_software = False
for found, err_msg in required_software():
if not found:
if not found and err_msg:
missing_software = True
msg += "\n " + err_msg
msg += "\n"