summaryrefslogtreecommitdiff
path: root/lib/spack/spack/package_prefs.py
blob: c2997034feeaf4611c4f835c2eb71c7cde4bb9c4 (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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# 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)
import stat
import warnings

import spack.error
import spack.repo
from spack.config import ConfigError
from spack.util.path import canonicalize_path
from spack.version import Version

_lesser_spec_types = {"compiler": spack.spec.CompilerSpec, "version": Version}


def _spec_type(component):
    """Map from component name to spec type for package prefs."""
    return _lesser_spec_types.get(component, spack.spec.Spec)


class PackagePrefs:
    """Defines the sort order for a set of specs.

    Spack's package preference implementation uses PackagePrefss to
    define sort order. The PackagePrefs class looks at Spack's
    packages.yaml configuration and, when called on a spec, returns a key
    that can be used to sort that spec in order of the user's
    preferences.

    You can use it like this:

       # key function sorts CompilerSpecs for `mpich` in order of preference
       kf = PackagePrefs('mpich', 'compiler')
       compiler_list.sort(key=kf)

    Or like this:

       # key function to sort VersionLists for OpenMPI in order of preference.
       kf = PackagePrefs('openmpi', 'version')
       version_list.sort(key=kf)

    Optionally, you can sort in order of preferred virtual dependency
    providers.  To do that, provide 'providers' and a third argument
    denoting the virtual package (e.g., ``mpi``):

       kf = PackagePrefs('trilinos', 'providers', 'mpi')
       provider_spec_list.sort(key=kf)

    """

    def __init__(self, pkgname, component, vpkg=None, all=True):
        self.pkgname = pkgname
        self.component = component
        self.vpkg = vpkg
        self.all = all

        self._spec_order = None

    def __call__(self, spec):
        """Return a key object (an index) that can be used to sort spec.

        Sort is done in package order. We don't cache the result of
        this function as Python's sort functions already ensure that the
        key function is called at most once per sorted element.
        """
        if self._spec_order is None:
            self._spec_order = self._specs_for_pkg(
                self.pkgname, self.component, self.vpkg, self.all
            )
        spec_order = self._spec_order

        # integer is the index of the first spec in order that satisfies
        # spec, or it's a number larger than any position in the order.
        match_index = next(
            (i for i, s in enumerate(spec_order) if spec.intersects(s)), len(spec_order)
        )
        if match_index < len(spec_order) and spec_order[match_index] == spec:
            # If this is called with multiple specs that all satisfy the same
            # minimum index in spec_order, the one which matches that element
            # of spec_order exactly is considered slightly better. Note
            # that because this decreases the value by less than 1, it is not
            # better than a match which occurs at an earlier index.
            match_index -= 0.5
        return match_index

    @classmethod
    def order_for_package(cls, pkgname, component, vpkg=None, all=True):
        """Given a package name, sort component (e.g, version, compiler, ...),
        and an optional vpkg, return the list from the packages config.
        """
        pkglist = [pkgname]
        if all:
            pkglist.append("all")

        for pkg in pkglist:
            pkg_entry = spack.config.get("packages").get(pkg)
            if not pkg_entry:
                continue

            order = pkg_entry.get(component)
            if not order:
                continue

            # vpkg is one more level
            if vpkg is not None:
                order = order.get(vpkg)

            if order:
                ret = [str(s).strip() for s in order]
                if component == "target":
                    ret = ["target=%s" % tname for tname in ret]
                return ret

        return []

    @classmethod
    def _specs_for_pkg(cls, pkgname, component, vpkg=None, all=True):
        """Given a sort order specified by the pkgname/component/second_key,
        return a list of CompilerSpecs, VersionLists, or Specs for
        that sorting list.
        """
        pkglist = cls.order_for_package(pkgname, component, vpkg, all)
        spec_type = _spec_type(component)
        return [spec_type(s) for s in pkglist]

    @classmethod
    def has_preferred_providers(cls, pkgname, vpkg):
        """Whether specific package has a preferred vpkg providers."""
        return bool(cls.order_for_package(pkgname, "providers", vpkg, False))

    @classmethod
    def has_preferred_targets(cls, pkg_name):
        """Whether specific package has a preferred vpkg providers."""
        return bool(cls.order_for_package(pkg_name, "target"))

    @classmethod
    def preferred_variants(cls, pkg_name):
        """Return a VariantMap of preferred variants/values for a spec."""
        for pkg_cls in (pkg_name, "all"):
            variants = spack.config.get("packages").get(pkg_cls, {}).get("variants", "")
            if variants:
                break

        # allow variants to be list or string
        if not isinstance(variants, str):
            variants = " ".join(variants)

        # Only return variants that are actually supported by the package
        pkg_cls = spack.repo.PATH.get_pkg_class(pkg_name)
        spec = spack.spec.Spec("%s %s" % (pkg_name, variants))
        return dict(
            (name, variant) for name, variant in spec.variants.items() if name in pkg_cls.variants
        )


def spec_externals(spec):
    """Return a list of external specs (w/external directory path filled in),
    one for each known external installation.
    """
    # break circular import.
    from spack.util.module_cmd import path_from_modules  # noqa: F401

    def _package(maybe_abstract_spec):
        pkg_cls = spack.repo.PATH.get_pkg_class(spec.name)
        return pkg_cls(maybe_abstract_spec)

    allpkgs = spack.config.get("packages")
    names = set([spec.name])
    names |= set(vspec.name for vspec in _package(spec).virtuals_provided)

    external_specs = []
    for name in names:
        pkg_config = allpkgs.get(name, {})
        pkg_externals = pkg_config.get("externals", [])
        for entry in pkg_externals:
            spec_str = entry["spec"]
            external_path = entry.get("prefix", None)
            if external_path:
                external_path = canonicalize_path(external_path)
            external_modules = entry.get("modules", None)
            external_spec = spack.spec.Spec.from_detection(
                spack.spec.Spec(
                    spec_str, external_path=external_path, external_modules=external_modules
                ),
                extra_attributes=entry.get("extra_attributes", {}),
            )
            if external_spec.intersects(spec):
                external_specs.append(external_spec)

    # Defensively copy returned specs
    return [s.copy() for s in external_specs]


def is_spec_buildable(spec):
    """Return true if the spec is configured as buildable"""
    allpkgs = spack.config.get("packages")
    all_buildable = allpkgs.get("all", {}).get("buildable", True)
    so_far = all_buildable  # the default "so far"

    def _package(s):
        pkg_cls = spack.repo.PATH.get_pkg_class(s.name)
        return pkg_cls(s)

    # check whether any providers for this package override the default
    if any(
        _package(spec).provides(name) and entry.get("buildable", so_far) != so_far
        for name, entry in allpkgs.items()
    ):
        so_far = not so_far

    spec_buildable = allpkgs.get(spec.name, {}).get("buildable", so_far)
    return spec_buildable


def get_package_dir_permissions(spec):
    """Return the permissions configured for the spec.

    Include the GID bit if group permissions are on. This makes the group
    attribute sticky for the directory. Package-specific settings take
    precedent over settings for ``all``"""
    perms = get_package_permissions(spec)
    if perms & stat.S_IRWXG and spack.config.get("config:allow_sgid", True):
        perms |= stat.S_ISGID
        if spec.concrete and "/afs/" in spec.prefix:
            warnings.warn(
                "Directory {0} seems to be located on AFS. If you"
                " encounter errors, try disabling the allow_sgid option"
                " using: spack config add 'config:allow_sgid:false'".format(spec.prefix)
            )
    return perms


def get_package_permissions(spec):
    """Return the permissions configured for the spec.

    Package-specific settings take precedence over settings for ``all``"""

    # Get read permissions level
    for name in (spec.name, "all"):
        try:
            readable = spack.config.get("packages:%s:permissions:read" % name, "")
            if readable:
                break
        except AttributeError:
            readable = "world"

    # Get write permissions level
    for name in (spec.name, "all"):
        try:
            writable = spack.config.get("packages:%s:permissions:write" % name, "")
            if writable:
                break
        except AttributeError:
            writable = "user"

    perms = stat.S_IRWXU
    if readable in ("world", "group"):  # world includes group
        perms |= stat.S_IRGRP | stat.S_IXGRP
    if readable == "world":
        perms |= stat.S_IROTH | stat.S_IXOTH

    if writable in ("world", "group"):
        if readable == "user":
            raise ConfigError(
                "Writable permissions may not be more"
                + " permissive than readable permissions.\n"
                + "      Violating package is %s" % spec.name
            )
        perms |= stat.S_IWGRP
    if writable == "world":
        if readable != "world":
            raise ConfigError(
                "Writable permissions may not be more"
                + " permissive than readable permissions.\n"
                + "      Violating package is %s" % spec.name
            )
        perms |= stat.S_IWOTH

    return perms


def get_package_group(spec):
    """Return the unix group associated with the spec.

    Package-specific settings take precedence over settings for ``all``"""
    for name in (spec.name, "all"):
        try:
            group = spack.config.get("packages:%s:permissions:group" % name, "")
            if group:
                break
        except AttributeError:
            group = ""
    return group


class VirtualInPackagesYAMLError(spack.error.SpackError):
    """Raised when a disallowed virtual is found in packages.yaml"""