summaryrefslogtreecommitdiff
path: root/lib/spack/spack/bootstrap/environment.py
blob: 36bbc0d260636246c4e6eb194f72d2fbf345f03b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# Copyright 2013-2023 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)
"""Bootstrap non-core Spack dependencies from an environment."""
import glob
import hashlib
import os
import pathlib
import sys
import warnings
from typing import List

import archspec.cpu

from llnl.util import tty

import spack.build_environment
import spack.environment
import spack.tengine
import spack.util.executable
from spack.environment import depfile

from ._common import _root_spec
from .config import root_path, spec_for_current_python, store_path


class BootstrapEnvironment(spack.environment.Environment):
    """Environment to install dependencies of Spack for a given interpreter and architecture"""

    @classmethod
    def spack_dev_requirements(cls) -> List[str]:
        """Spack development requirements"""
        return [
            isort_root_spec(),
            mypy_root_spec(),
            black_root_spec(),
            flake8_root_spec(),
            pytest_root_spec(),
        ]

    @classmethod
    def environment_root(cls) -> pathlib.Path:
        """Environment root directory"""
        bootstrap_root_path = root_path()
        python_part = spec_for_current_python().replace("@", "")
        arch_part = 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)
            )
        )

    @classmethod
    def view_root(cls) -> pathlib.Path:
        """Location of the view"""
        return cls.environment_root().joinpath("view")

    @classmethod
    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, "**"))
        result = glob.glob(glob_expr)
        if not result:
            msg = f"Cannot find any Python path in {cls.view_root()}"
            warnings.warn(msg)
        return result

    @classmethod
    def bin_dirs(cls) -> List[pathlib.Path]:
        """Paths to be added to PATH"""
        return [cls.view_root().joinpath("bin")]

    @classmethod
    def spack_yaml(cls) -> pathlib.Path:
        """Environment spack.yaml file"""
        return cls.environment_root().joinpath("spack.yaml")

    def __init__(self) -> None:
        if not self.spack_yaml().exists():
            self._write_spack_yaml_file()
        super().__init__(self.environment_root())

    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``
        method of environments on Windows.
        """
        with tty.SuppressOutput(msg_enabled=False, warn_enabled=False):
            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)
            if sys.platform == "win32":
                self.install_all()
            else:
                self._install_with_depfile()
            self.write(regenerate=True)

    def update_syspath_and_environ(self) -> None:
        """Update ``sys.path`` and the PATH, PYTHONPATH environment variables to point to
        the environment view.
        """
        # Do minimal modifications to sys.path and environment variables. In particular, pay
        # attention to have the smallest PYTHONPATH / sys.path possible, since that may impact
        # the performance of the current interpreter
        sys.path.extend(self.pythonpaths())
        os.environ["PATH"] = os.pathsep.join(
            [str(x) for x in self.bin_dirs()] + os.environ.get("PATH", "").split(os.pathsep)
        )
        os.environ["PYTHONPATH"] = os.pathsep.join(
            os.environ.get("PYTHONPATH", "").split(os.pathsep)
            + [str(x) for x in self.pythonpaths()]
        )

    def _install_with_depfile(self) -> None:
        model = depfile.MakefileModel.from_env(self)
        template = spack.tengine.make_environment().get_template(
            os.path.join("depfile", "Makefile")
        )
        makefile = self.environment_root() / "Makefile"
        makefile.write_text(template.render(model.to_dict()))
        make = spack.util.executable.which("make")
        kwargs = {}
        if not tty.is_debug():
            kwargs = {"output": os.devnull, "error": os.devnull}
        make(
            "-C",
            str(self.environment_root()),
            "-j",
            str(spack.build_environment.determine_number_of_jobs(parallel=True)),
            **kwargs,
        )

    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": spec_for_current_python(),
            "python_prefix": sys.exec_prefix,
            "architecture": archspec.cpu.host().family,
            "environment_path": self.environment_root(),
            "environment_specs": self.spack_dev_requirements(),
            "store_path": store_path(),
        }
        self.environment_root().mkdir(parents=True, exist_ok=True)
        self.spack_yaml().write_text(template.render(context), encoding="utf-8")


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() -> str:
    """Return the root spec used to bootstrap mypy"""
    return _root_spec("py-mypy@0.900:")


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() -> str:
    """Return the root spec used to bootstrap flake8"""
    return _root_spec("py-flake8")


def pytest_root_spec() -> str:
    """Return the root spec used to bootstrap flake8"""
    return _root_spec("py-pytest")


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()
        env.update_syspath_and_environ()