Source code for spack.bootstrap.environment

# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Bootstrap non-core Spack dependencies from an environment."""

import contextlib
import hashlib
import os
import pathlib
import shutil
import sys
from typing import Iterable, List

import spack.vendor.archspec.cpu

import spack.binary_distribution
import spack.config
import spack.environment
import spack.spec
import spack.tengine
import spack.util.gpg
import spack.util.path
from spack.llnl.util import tty

from .config import root_path, spec_for_current_python, store_path
from .core import _add_externals_if_missing


[docs] class BootstrapEnvironment(spack.environment.Environment): """Environment to install dependencies of Spack for a given interpreter and architecture""" def __init__(self) -> None: if not self.spack_yaml().exists(): self._write_spack_yaml_file() super().__init__(self.environment_root()) # Remove python package roots created before python-venv was introduced for s in self.concrete_roots(): if "python" in s.package.extendees and not s.dependencies("python-venv"): self.deconcretize_by_hash(s.dag_hash())
[docs] @classmethod def spack_dev_requirements(cls) -> List[str]: """Spack development requirements""" return [pytest_root_spec(), ruff_root_spec(), mypy_root_spec()]
[docs] @classmethod def environment_root(cls) -> pathlib.Path: """Environment root directory""" bootstrap_root_path = root_path() python_part = spec_for_current_python().replace("@", "") arch_part = spack.vendor.archspec.cpu.host().family interpreter_part = hashlib.md5(sys.exec_prefix.encode()).hexdigest()[:5] environment_dir = f"{python_part}-{arch_part}-{interpreter_part}" return pathlib.Path( spack.util.path.canonicalize_path( os.path.join(bootstrap_root_path, "environments", environment_dir) ) )
[docs] @classmethod def view_root(cls) -> pathlib.Path: """Location of the view""" return cls.environment_root().joinpath("view")
[docs] @classmethod def bin_dir(cls) -> pathlib.Path: """Paths to be added to PATH""" return cls.view_root().joinpath("bin")
[docs] def python_dirs(self) -> Iterable[pathlib.Path]: python = next(s for s in self.all_specs_generator() if s.name == "python-venv").package return {self.view_root().joinpath(p) for p in (python.platlib, python.purelib)}
[docs] @classmethod def spack_yaml(cls) -> pathlib.Path: """Environment spack.yaml file""" return cls.environment_root().joinpath("spack.yaml")
[docs] @contextlib.contextmanager def trust_bootstrap_mirror_keys(self): with spack.util.gpg.gnupghome_override(os.path.join(root_path(), ".bootstrap-gpg")): spack.binary_distribution.get_keys(install=True, trust=True) yield
[docs] def update_installations(self) -> None: """Update the installations of this environment.""" log_enabled = tty.is_debug() or tty.is_verbose() with tty.SuppressOutput(msg_enabled=log_enabled, warn_enabled=log_enabled): specs = self.concretize() if specs: colorized_specs = [ spack.spec.Spec(x).cformat("{name}{@version}") for x in self.spack_dev_requirements() ] tty.msg(f"[BOOTSTRAPPING] Installing dependencies ({', '.join(colorized_specs)})") self.write(regenerate=False) with tty.SuppressOutput(msg_enabled=log_enabled, warn_enabled=log_enabled): with self.trust_bootstrap_mirror_keys(): fetch_policy = ( "cache_only" if not spack.config.get("bootstrap:dev:enable_source", False) else "auto" ) try: self.install_all( fail_fast=True, root_policy=fetch_policy, dependencies_policy=fetch_policy, ) except BaseException: # catch any exception as we always want to clean up shutil.rmtree(self.environment_root()) raise self.write(regenerate=True)
[docs] def load(self) -> None: """Update PATH and sys.path.""" # Make executables available (shouldn't need PYTHONPATH) os.environ["PATH"] = f"{self.bin_dir()}{os.pathsep}{os.environ.get('PATH', '')}" # Spack itself imports pytest sys.path.extend(str(p) for p in self.python_dirs())
def _write_spack_yaml_file(self) -> None: tty.msg( "[BOOTSTRAPPING] Spack has missing dependencies, creating a bootstrapping environment" ) env = spack.tengine.make_environment() template = env.get_template("bootstrap/spack.yaml") context = { "python_spec": f"{spec_for_current_python()}+ctypes", "python_prefix": sys.exec_prefix, "architecture": spack.vendor.archspec.cpu.host().family, "environment_path": self.environment_root(), "environment_specs": self.spack_dev_requirements(), "store_path": store_path(), "bootstrap_mirrors": dev_bootstrap_mirror_names(), } self.environment_root().mkdir(parents=True, exist_ok=True) self.spack_yaml().write_text(template.render(context), encoding="utf-8")
[docs] def mypy_root_spec() -> str: """Return the root spec used to bootstrap mypy""" return "py-mypy@0.900: ^py-mypy-extensions@:1.0"
[docs] def pytest_root_spec() -> str: """Return the root spec used to bootstrap pytest""" return "py-pytest@6.2.4:"
[docs] def ruff_root_spec() -> str: """Return the root spec used to bootstrap ruff""" return "py-ruff@0.15.0"
[docs] def dev_bootstrap_mirror_names() -> List[str]: """Return the mirror names used for bootstrapping dev requirements""" return [ "developer-tools-darwin", "developer-tools-x86_64_v3-linux-gnu", "developer-tools-aarch64-linux-gnu", ]
[docs] def ensure_environment_dependencies() -> None: """Ensure Spack dependencies from the bootstrap environment are installed and ready to use""" _add_externals_if_missing() with BootstrapEnvironment() as env: env.update_installations() env.load()