summaryrefslogtreecommitdiff
path: root/lib/spack/spack/cmd/external.py
blob: 29be54ba6438060afbfffd023232385a154f5e8f (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
# Copyright 2013-2024 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 argparse
import errno
import os
import re
import sys
from typing import List, Optional

import llnl.util.tty as tty
import llnl.util.tty.colify as colify

import spack
import spack.cmd
import spack.config
import spack.cray_manifest as cray_manifest
import spack.detection
import spack.error
import spack.util.environment
from spack.cmd.common import arguments

description = "manage external packages in Spack configuration"
section = "config"
level = "short"


def setup_parser(subparser):
    sp = subparser.add_subparsers(metavar="SUBCOMMAND", dest="external_command")

    find_parser = sp.add_parser("find", help="add external packages to packages.yaml")
    find_parser.add_argument(
        "--not-buildable",
        action="store_true",
        default=False,
        help="packages with detected externals won't be built with Spack",
    )
    find_parser.add_argument("--exclude", action="append", help="packages to exclude from search")
    find_parser.add_argument(
        "-p",
        "--path",
        default=None,
        action="append",
        help="one or more alternative search paths for finding externals",
    )
    find_parser.add_argument(
        "--scope",
        action=arguments.ConfigScope,
        default=lambda: spack.config.default_modify_scope("packages"),
        help="configuration scope to modify",
    )
    find_parser.add_argument(
        "--all", action="store_true", help="search for all packages that Spack knows about"
    )
    arguments.add_common_arguments(find_parser, ["tags", "jobs"])
    find_parser.add_argument("packages", nargs=argparse.REMAINDER)
    find_parser.epilog = (
        'The search is by default on packages tagged with the "build-tools" or '
        '"core-packages" tags. Use the --all option to search for every possible '
        "package Spack knows how to find."
    )

    sp.add_parser("list", help="list detectable packages, by repository and name")

    read_cray_manifest = sp.add_parser(
        "read-cray-manifest",
        help="consume a Spack-compatible description of externally-installed packages, including "
        "dependency relationships",
    )
    read_cray_manifest.add_argument(
        "--file", default=None, help="specify a location other than the default"
    )
    read_cray_manifest.add_argument(
        "--directory", default=None, help="specify a directory storing a group of manifest files"
    )
    read_cray_manifest.add_argument(
        "--ignore-default-dir",
        action="store_true",
        default=False,
        help="ignore the default directory of manifest files",
    )
    read_cray_manifest.add_argument(
        "--dry-run",
        action="store_true",
        default=False,
        help="don't modify DB with files that are read",
    )
    read_cray_manifest.add_argument(
        "--fail-on-error",
        action="store_true",
        help="if a manifest file cannot be parsed, fail and report the full stack trace",
    )


def external_find(args):
    if args.all or not (args.tags or args.packages):
        # If the user calls 'spack external find' with no arguments, and
        # this system has a description of installed packages, then we should
        # consume it automatically.
        try:
            _collect_and_consume_cray_manifest_files()
        except NoManifestFileError:
            # It's fine to not find any manifest file if we are doing the
            # search implicitly (i.e. as part of 'spack external find')
            pass
        except Exception as e:
            # For most exceptions, just print a warning and continue.
            # Note that KeyboardInterrupt does not subclass Exception
            # (so CTRL-C will terminate the program as expected).
            skip_msg = "Skipping manifest and continuing with other external checks"
            if (isinstance(e, IOError) or isinstance(e, OSError)) and e.errno in [
                errno.EPERM,
                errno.EACCES,
            ]:
                # The manifest file does not have sufficient permissions enabled:
                # print a warning and keep going
                tty.warn("Unable to read manifest due to insufficient permissions.", skip_msg)
            else:
                tty.warn("Unable to read manifest, unexpected error: {0}".format(str(e)), skip_msg)

    # Outside the Cray manifest, the search is done by tag for performance reasons,
    # since tags are cached.

    # If the user specified both --all and --tag, then --all has precedence
    if args.all or args.packages:
        # Each detectable package has at least the detectable tag
        args.tags = ["detectable"]
    elif not args.tags:
        # If the user didn't specify anything, search for build tools by default
        args.tags = ["core-packages", "build-tools"]

    candidate_packages = packages_to_search_for(
        names=args.packages, tags=args.tags, exclude=args.exclude
    )
    detected_packages = spack.detection.by_path(
        candidate_packages, path_hints=args.path, max_workers=args.jobs
    )

    new_entries = spack.detection.update_configuration(
        detected_packages, scope=args.scope, buildable=not args.not_buildable
    )
    if new_entries:
        path = spack.config.CONFIG.get_config_filename(args.scope, "packages")
        msg = "The following specs have been detected on this system and added to {0}"
        tty.msg(msg.format(path))
        spack.cmd.display_specs(new_entries)
    else:
        tty.msg("No new external packages detected")


def packages_to_search_for(
    *, names: Optional[List[str]], tags: List[str], exclude: Optional[List[str]]
):
    result = []
    for current_tag in tags:
        result.extend(spack.repo.PATH.packages_with_tags(current_tag, full=True))

    if names:
        # Match both fully qualified and unqualified
        parts = [rf"(^{x}$|[.]{x}$)" for x in names]
        select_re = re.compile("|".join(parts))
        result = [x for x in result if select_re.search(x)]

    if exclude:
        # Match both fully qualified and unqualified
        parts = [rf"(^{x}$|[.]{x}$)" for x in exclude]
        select_re = re.compile("|".join(parts))
        result = [x for x in result if not select_re.search(x)]

    return result


def external_read_cray_manifest(args):
    _collect_and_consume_cray_manifest_files(
        manifest_file=args.file,
        manifest_directory=args.directory,
        dry_run=args.dry_run,
        fail_on_error=args.fail_on_error,
        ignore_default_dir=args.ignore_default_dir,
    )


def _collect_and_consume_cray_manifest_files(
    manifest_file=None,
    manifest_directory=None,
    dry_run=False,
    fail_on_error=False,
    ignore_default_dir=False,
):
    manifest_files = []
    if manifest_file:
        manifest_files.append(manifest_file)

    manifest_dirs = []
    if manifest_directory:
        manifest_dirs.append(manifest_directory)

    if not ignore_default_dir and os.path.isdir(cray_manifest.default_path):
        tty.debug(
            "Cray manifest path {0} exists: collecting all files to read.".format(
                cray_manifest.default_path
            )
        )
        manifest_dirs.append(cray_manifest.default_path)
    else:
        tty.debug(
            "Default Cray manifest directory {0} does not exist.".format(
                cray_manifest.default_path
            )
        )

    for directory in manifest_dirs:
        for fname in os.listdir(directory):
            if fname.endswith(".json"):
                fpath = os.path.join(directory, fname)
                tty.debug("Adding manifest file: {0}".format(fpath))
                manifest_files.append(os.path.join(directory, fpath))

    if not manifest_files:
        raise NoManifestFileError(
            "--file/--directory not specified, and no manifest found at {0}".format(
                cray_manifest.default_path
            )
        )

    for path in manifest_files:
        tty.debug("Reading manifest file: " + path)
        try:
            cray_manifest.read(path, not dry_run)
        except spack.error.SpackError as e:
            if fail_on_error:
                raise
            else:
                tty.warn("Failure reading manifest file: {0}\n\t{1}".format(path, str(e)))


def external_list(args):
    # Trigger a read of all packages, might take a long time.
    list(spack.repo.PATH.all_package_classes())
    # Print all the detectable packages
    tty.msg("Detectable packages per repository")
    for namespace, pkgs in sorted(spack.package_base.detectable_packages.items()):
        print("Repository:", namespace)
        colify.colify(pkgs, indent=4, output=sys.stdout)


def external(parser, args):
    action = {
        "find": external_find,
        "list": external_list,
        "read-cray-manifest": external_read_cray_manifest,
    }
    action[args.external_command](args)


class NoManifestFileError(spack.error.SpackError):
    pass