# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import argparse
import fnmatch
import json
import math
import os
import re
import sys
from html import escape
from typing import Optional, Type
import spack.deptypes as dt
import spack.package_base
import spack.repo
import spack.util.git
from spack.cmd.common import arguments
from spack.util import tty
from spack.util.filesystem import working_dir
from spack.util.tty.colify import colify
from spack.util.url import path_to_file_url
from spack.version import VersionList
description = "list and search available packages"
section = "query"
level = "short"
formatters = {}
[docs]
def setup_parser(subparser: argparse.ArgumentParser) -> None:
subparser.add_argument(
"filter",
nargs=argparse.REMAINDER,
help="optional case-insensitive glob patterns to filter results",
)
subparser.add_argument(
"-r",
"--repo",
"-N",
"--namespace",
dest="repos",
action="append",
default=[],
help="only list packages from the specified repo/namespace",
)
subparser.add_argument(
"-d",
"--search-description",
action="store_true",
default=False,
help="filtering will also search the description for a match",
)
subparser.add_argument(
"--format",
default="name_only",
choices=formatters,
help="format to be used to print the output [default: name_only]",
)
subparser.add_argument(
"-v",
"--virtuals",
action="store_true",
default=False,
help="include virtual packages in list",
)
arguments.add_common_arguments(subparser, ["tags"])
# Doesn't really make sense to update in count mode.
count_or_update = subparser.add_mutually_exclusive_group()
count_or_update.add_argument(
"--count",
action="store_true",
default=False,
help="display the number of packages that would be listed",
)
count_or_update.add_argument(
"--update",
metavar="FILE",
default=None,
action="store",
help="write output to the specified file, if any package is newer",
)
[docs]
def filter_by_name(pkgs, args):
"""
Filters the sequence of packages according to user prescriptions
Args:
pkgs: sequence of packages
args: parsed command line arguments
Returns:
filtered and sorted list of packages
"""
if args.filter:
res = []
for f in args.filter:
if "*" not in f and "?" not in f:
r = fnmatch.translate("*" + f + "*")
else:
r = fnmatch.translate(f)
rc = re.compile(r, flags=re.IGNORECASE)
res.append(rc)
if args.search_description:
def match(p, f):
if f.match(p):
return True
pkg_cls = spack.repo.PATH.get_pkg_class(p)
if pkg_cls.__doc__:
return f.match(pkg_cls.__doc__)
return False
else:
def match(p, f):
return f.match(p)
pkgs = [p for p in pkgs if any(match(p, f) for f in res)]
return sorted(pkgs, key=lambda s: s.lower())
[docs]
@formatter
def name_only(pkgs, out):
indent = 0
colify(pkgs, indent=indent, output=out)
if out.isatty():
tty.msg("%d packages" % len(pkgs))
[docs]
def github_url(pkg: Type[spack.package_base.PackageBase]) -> Optional[str]:
"""Link to a package file in spack package's github or the path to the file.
Args:
pkg: package instance
Returns: URL to the package file on github or the local file path; otherwise, ``None``.
"""
git = None
module_path = f"{pkg.__module__.replace('.', '/')}.py"
for repo in spack.repo.PATH.repos:
if not repo.python_path:
continue
path = os.path.join(repo.python_path, module_path)
if not os.path.exists(path):
continue
git = git or spack.util.git.git()
if not git:
tty.debug("Cannot determine package URL for {pkg} without 'git', using path URL")
return path_to_file_url(path)
tty.debug(f"Checking git for repository path '{path}'")
with working_dir(os.path.dirname(path)):
origin_url = git(
"config",
"--get",
"remote.origin.url",
output=str,
error=os.devnull,
fail_on_error=False,
)
if not origin_url:
tty.debug("Cannot determine remote origin url, using path URL")
return path_to_file_url(path)
# Handle spack repositories cloned with any scheme (e.g., ssh) by
# ignoring the scheme designation.
if any([name in origin_url for name in ["spack.git", "spack-packages.git"]]):
git_repo = (origin_url.split("/")[-1]).replace(".git", "").strip()
prefix = git(
"rev-parse", "--show-prefix", output=str, error=os.devnull, fail_on_error=False
)
return (
f"https://github.com/spack/{git_repo}/blob/develop/{prefix.strip()}package.py"
)
tty.debug(f"Unrecognized repository for {pkg}, using path URL")
return path_to_file_url(path)
tty.debug(f"Unable to determine the package repository URL for {pkg}")
return None
[docs]
def rows_for_ncols(elts, ncols):
"""Print out rows in a table with ncols of elts laid out vertically."""
clen = int(math.ceil(len(elts) / ncols))
for r in range(clen):
row = []
for c in range(ncols):
i = c * clen + r
row.append(elts[i] if i < len(elts) else None)
yield row
[docs]
def get_dependencies(pkg):
all_deps = {}
for deptype in dt.ALL_TYPES:
deps = pkg.dependencies_of_type(dt.flag_from_string(deptype))
all_deps[deptype] = [d for d in deps]
return all_deps
[docs]
@formatter
def version_json(pkg_names, out):
"""Print all packages with their latest versions."""
pkg_classes = [spack.repo.PATH.get_pkg_class(name) for name in pkg_names]
out.write("[\n")
# output name and latest version for each package
pkg_latest = ",\n".join(
[
' {{"name": "{0}",\n'
' "latest_version": "{1}",\n'
' "versions": {2},\n'
' "homepage": "{3}",\n'
' "file": "{4}",\n'
' "maintainers": {5},\n'
' "dependencies": {6}'
"}}".format(
pkg_cls.name,
VersionList(pkg_cls.versions).preferred(),
json.dumps([str(v) for v in reversed(sorted(pkg_cls.versions))]),
pkg_cls.homepage,
github_url(pkg_cls),
json.dumps(pkg_cls.maintainers),
json.dumps(get_dependencies(pkg_cls)),
)
for pkg_cls in pkg_classes
]
)
out.write(pkg_latest)
# important: no trailing comma in JSON arrays
out.write("\n]\n")
[docs]
@formatter
def html(pkg_names, out):
"""Print out information on all packages in Sphinx HTML.
This is intended to be inlined directly into Sphinx documentation.
We write HTML instead of RST for speed; generating RST from *all*
packages causes the Sphinx build to take forever. Including this as
raw HTML is much faster.
"""
# Read in all packages
pkg_classes = [spack.repo.PATH.get_pkg_class(name) for name in pkg_names]
# Start at 2 because the title of the page from Sphinx is id1.
span_id = 2
# HTML header with an increasing id span
def head(n, span_id, title, anchor=None):
if anchor is None:
anchor = title
out.write(
(
'<span id="id%d"></span>'
'<h1>%s<a class="headerlink" href="#%s" '
'title="Permalink to this headline">¶</a>'
"</h1>\n"
)
% (span_id, title, anchor)
)
# Start with the number of packages, skipping the title and intro
# blurb, which we maintain in the RST file.
out.write("<p>\n")
out.write("Spack currently has %d mainline packages:\n" % len(pkg_classes))
out.write("</p>\n")
# Table of links to all packages
out.write('<table border="1" class="docutils">\n')
out.write('<tbody valign="top">\n')
for i, row in enumerate(rows_for_ncols(pkg_names, 3)):
out.write('<tr class="row-odd">\n' if i % 2 == 0 else '<tr class="row-even">\n')
for name in row:
out.write("<td>\n")
out.write('<a class="reference internal" href="#%s">%s</a></td>\n' % (name, name))
out.write("</td>\n")
out.write("</tr>\n")
out.write("</tbody>\n")
out.write("</table>\n")
out.write('<hr class="docutils"/>\n')
# Output some text for each package.
for pkg_cls in pkg_classes:
out.write('<div class="section" id="%s">\n' % pkg_cls.name)
head(2, span_id, pkg_cls.name)
span_id += 1
out.write('<dl class="docutils">\n')
out.write("<dt>Homepage:</dt>\n")
out.write('<dd><ul class="first last simple">\n')
if pkg_cls.homepage:
out.write(
('<li><a class="reference external" href="%s">%s</a></li>\n')
% (pkg_cls.homepage, escape(pkg_cls.homepage, True))
)
else:
out.write("No homepage\n")
out.write("</ul></dd>\n")
out.write("<dt>Spack package:</dt>\n")
out.write('<dd><ul class="first last simple">\n')
out.write(
('<li><a class="reference external" href="%s">%s/package.py</a></li>\n')
% (github_url(pkg_cls), pkg_cls.name)
)
out.write("</ul></dd>\n")
if pkg_cls.versions:
out.write("<dt>Versions:</dt>\n")
out.write("<dd>\n")
out.write(", ".join(str(v) for v in reversed(sorted(pkg_cls.versions))))
out.write("\n")
out.write("</dd>\n")
for deptype in dt.ALL_TYPES:
deps = pkg_cls.dependencies_of_type(dt.flag_from_string(deptype))
if deps:
out.write("<dt>%s Dependencies:</dt>\n" % deptype.capitalize())
out.write("<dd>\n")
out.write(
", ".join(
(
d
if d not in pkg_names
else '<a class="reference internal" href="#%s">%s</a>' % (d, d)
)
for d in deps
)
)
out.write("\n")
out.write("</dd>\n")
out.write("<dt>Description:</dt>\n")
out.write("<dd>\n")
out.write(escape(pkg_cls.format_doc(indent=2), True))
out.write("\n")
out.write("</dd>\n")
out.write("</dl>\n")
out.write('<hr class="docutils"/>\n')
out.write("</div>\n")
[docs]
def list(parser, args):
# retrieve the formatter to use from args
formatter = formatters[args.format]
# Retrieve the names of all the packages
repos = [spack.repo.PATH]
if args.repos:
repos = [spack.repo.PATH.get_repo(name) for name in args.repos]
pkgs = {name for repo in repos for name in repo.all_package_names(args.virtuals)}
# Filter the set appropriately
sorted_packages = filter_by_name(pkgs, args)
# If tags have been specified on the command line, filter by tags
if args.tags:
packages_with_tags = spack.repo.PATH.packages_with_tags(*args.tags)
sorted_packages = [p for p in sorted_packages if p in packages_with_tags]
if args.update:
# change output stream if user asked for update
if os.path.exists(args.update):
if os.path.getmtime(args.update) > spack.repo.PATH.last_mtime():
tty.msg("File is up to date: %s" % args.update)
return
tty.msg("Updating file: %s" % args.update)
with open(args.update, "w", encoding="utf-8") as f:
formatter(sorted_packages, f)
elif args.count:
# just print the number of packages in the result
print(len(sorted_packages))
else:
# print formatted package list
formatter(sorted_packages, sys.stdout)