# 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 sys
import textwrap
from itertools import zip_longest
import llnl.util.tty as tty
import llnl.util.tty.color as color
from llnl.util.tty.colify import colify
import spack.deptypes as dt
import spack.fetch_strategy as fs
import spack.install_test
import spack.repo
import spack.spec
import spack.version
from spack.cmd.common import arguments
from spack.package_base import preferred_version
description = "get detailed information on a particular package"
section = "basic"
level = "short"
header_color = "@*b"
plain_format = "@."
def padder(str_list, extra=0):
"""Return a function to pad elements of a list."""
length = max(len(str(s)) for s in str_list) + extra
def pad(string):
string = str(string)
padding = max(0, length - len(string))
return string + (padding * " ")
return pad
def setup_parser(subparser):
subparser.add_argument(
"-a", "--all", action="store_true", default=False, help="output all package information"
)
options = [
("--detectable", print_detectable.__doc__),
("--maintainers", print_maintainers.__doc__),
("--no-dependencies", "do not " + print_dependencies.__doc__),
("--no-variants", "do not " + print_variants.__doc__),
("--no-versions", "do not " + print_versions.__doc__),
("--phases", print_phases.__doc__),
("--tags", print_tags.__doc__),
("--tests", print_tests.__doc__),
("--virtuals", print_virtuals.__doc__),
("--variants-by-name", "list variants in strict name order; don't group by condition"),
]
for opt, help_comment in options:
subparser.add_argument(opt, action="store_true", help=help_comment)
arguments.add_common_arguments(subparser, ["package"])
def section_title(s):
return header_color + s + plain_format
def version(s):
return spack.spec.VERSION_COLOR + s + plain_format
def variant(s):
return spack.spec.ENABLED_VARIANT_COLOR + s + plain_format
def license(s):
return spack.spec.VERSION_COLOR + s + plain_format
class VariantFormatter:
def __init__(self, pkg):
self.variants = pkg.variants
self.headers = ("Name [Default]", "When", "Allowed values", "Description")
# Don't let name or possible values be less than max widths
_, cols = tty.terminal_size()
max_name = min(self.column_widths[0], 30)
max_when = min(self.column_widths[1], 30)
max_vals = min(self.column_widths[2], 20)
# allow the description column to extend as wide as the terminal.
max_description = min(
self.column_widths[3],
# min width 70 cols, 14 cols of margins and column spacing
max(cols, 70) - max_name - max_vals - 14,
)
self.column_widths = (max_name, max_when, max_vals, max_description)
# Compute the format
self.fmt = "%%-%ss%%-%ss%%-%ss%%s" % (
self.column_widths[0] + 4,
self.column_widths[1] + 4,
self.column_widths[2] + 4,
)
def default(self, v):
s = "on" if v.default is True else "off"
if not isinstance(v.default, bool):
s = v.default
return s
@property
def lines(self):
if not self.variants:
yield " None"
return
else:
yield " " + self.fmt % self.headers
underline = tuple([w * "=" for w in self.column_widths])
yield " " + self.fmt % underline
yield ""
for k, e in sorted(self.variants.items()):
v, w = e
name = textwrap.wrap(
"{0} [{1}]".format(k, self.default(v)), width=self.column_widths[0]
)
if all(spec == spack.spec.Spec() for spec in w):
w = "--"
when = textwrap.wrap(str(w), width=self.column_widths[1])
allowed = v.allowed_values.replace("True, False", "on, off")
allowed = textwrap.wrap(allowed, width=self.column_widths[2])
description = []
for d_line in v.description.split("\n"):
description += textwrap.wrap(d_line, width=self.column_widths[3])
for t in zip_longest(name, when, allowed, description, fillvalue=""):
yield " " + self.fmt % t
def print_dependencies(pkg, args):
"""output build, link, and run package dependencies"""
for deptype in ("build", "link", "run"):
color.cprint("")
color.cprint(section_title("%s Dependencies:" % deptype.capitalize()))
deps = sorted(pkg.dependencies_of_type(dt.flag_from_string(deptype)))
if deps:
colify(deps, indent=4)
else:
color.cprint(" None")
def print_detectable(pkg, args):
"""output information on external detection"""
color.cprint("")
color.cprint(section_title("Externally Detectable: "))
# If the package has an 'executables' of 'libraries' field, it
# can detect an installation
if hasattr(pkg, "executables") or hasattr(pkg, "libraries"):
find_attributes = []
if hasattr(pkg, "determine_version"):
find_attributes.append("version")
if hasattr(pkg, "determine_variants"):
find_attributes.append("variants")
# If the package does not define 'determine_version' nor
# 'determine_variants', then it must use some custom detection
# mechanism. In this case, just inform the user it's detectable somehow.
color.cprint(
" True{0}".format(
" (" + ", ".join(find_attributes) + ")" if find_attributes else ""
)
)
else:
color.cprint(" False")
def print_maintainers(pkg, args):
"""output package maintainers"""
if len(pkg.maintainers) > 0:
mnt = " ".join(["@@" + m for m in pkg.maintainers])
color.cprint("")
color.cprint(section_title("Maintainers: ") + mnt)
def print_phases(pkg, args):
"""output installation phases"""
if hasattr(pkg.builder, "phases") and pkg.builder.phases:
color.cprint("")
color.cprint(section_title("Installation Phases:"))
phase_str = ""
for phase in pkg.builder.phases:
phase_str += " {0}".format(phase)
color.cprint(phase_str)
def print_tags(pkg, args):
"""output package tags"""
color.cprint("")
color.cprint(section_title("Tags: "))
if hasattr(pkg, "tags"):
tags = sorted(pkg.tags)
colify(tags, indent=4)
else:
color.cprint(" None")
def print_tests(pkg, args):
"""output relevant build-time and stand-alone tests"""
# Some built-in base packages (e.g., Autotools) define callback (e.g.,
# check) inherited by descendant packages. These checks may not result
# in build-time testing if the package's build does not implement the
# expected functionality (e.g., a 'check' or 'test' targets).
#
# So the presence of a callback in Spack does not necessarily correspond
# to the actual presence of built-time tests for a package.
for callbacks, phase in [
(getattr(pkg, "build_time_test_callbacks", None), "Build"),
(getattr(pkg, "install_time_test_callbacks", None), "Install"),
]:
color.cprint("")
color.cprint(section_title("Available {0} Phase Test Methods:".format(phase)))
names = []
if callbacks:
for name in callbacks:
if getattr(pkg, name, False):
names.append(name)
if names:
colify(sorted(names), indent=4)
else:
color.cprint(" None")
# PackageBase defines an empty install/smoke test but we want to know
# if it has been overridden and, therefore, assumed to be implemented.
color.cprint("")
color.cprint(section_title("Stand-Alone/Smoke Test Methods:"))
names = spack.install_test.test_function_names(pkg, add_virtuals=True)
if names:
colify(sorted(names), indent=4)
else:
color.cprint(" None")
def _fmt_value(v):
if v is None or isinstance(v, bool):
return str(v).lower()
else:
return str(v)
def _fmt_name_and_default(variant):
"""Print colorized name [default] for a variant."""
return color.colorize(f"@c{{{variant.name}}} @C{{[{_fmt_value(variant.default)}]}}")
def _fmt_when(when, indent):
return color.colorize(f"{indent * ' '}@B{{when}} {color.cescape(when)}")
def _fmt_variant_description(variant, width, indent):
"""Format a variant's description, preserving explicit line breaks."""
return "\n".join(
textwrap.fill(
line, width=width, initial_indent=indent * " ", subsequent_indent=indent * " "
)
for line in variant.description.split("\n")
)
def _fmt_variant(variant, max_name_default_len, indent, when=None, out=None):
out = out or sys.stdout
_, cols = tty.terminal_size()
name_and_default = _fmt_name_and_default(variant)
name_default_len = color.clen(name_and_default)
values = variant.values
if not isinstance(variant.values, (tuple, list, spack.variant.DisjointSetsOfValues)):
values = [variant.values]
# put 'none' first, sort the rest by value
sorted_values = sorted(values, key=lambda v: (v != "none", v))
pad = 4 # min padding between 'name [default]' and values
value_indent = (indent + max_name_default_len + pad) * " " # left edge of values
# This preserves any formatting (i.e., newlines) from how the description was
# written in package.py, but still wraps long lines for small terminals.
# This allows some packages to provide detailed help on their variants (see, e.g., gasnet).
formatted_values = "\n".join(
textwrap.wrap(
f"{', '.join(_fmt_value(v) for v in sorted_values)}",
width=cols - 2,
initial_indent=value_indent,
subsequent_indent=value_indent,
)
)
formatted_values = formatted_values[indent + name_default_len + pad :]
# name [default] value1, value2, value3, ...
padding = pad * " "
color.cprint(f"{indent * ' '}{name_and_default}{padding}@c{{{formatted_values}}}", stream=out)
# when <spec>
description_indent = indent + 4
if when is not None and when != spack.spec.Spec():
out.write(_fmt_when(when, description_indent - 2))
out.write("\n")
# description, preserving explicit line breaks from the way it's written in the package file
out.write(_fmt_variant_description(variant, cols - 2, description_indent))
out.write("\n")
def _variants_by_name_when(pkg):
"""Adaptor to get variants keyed by { name: { when: { [Variant...] } }."""
# TODO: replace with pkg.variants_by_name(when=True) when unified directive dicts are merged.
variants = {}
for name, (variant, whens) in sorted(pkg.variants.items()):
for when in whens:
variants.setdefault(name, {}).setdefault(when, []).append(variant)
return variants
def _variants_by_when_name(pkg):
"""Adaptor to get variants keyed by { when: { name: Variant } }"""
# TODO: replace with pkg.variants when unified directive dicts are merged.
variants = {}
for name, (variant, whens) in pkg.variants.items():
for when in whens:
variants.setdefault(when, {})[name] = variant
return variants
def _print_variants_header(pkg):
"""output variants"""
if not pkg.variants:
print(" None")
return
color.cprint("")
color.cprint(section_title("Variants:"))
variants_by_name = _variants_by_name_when(pkg)
# Calculate the max length of the "name [default]" part of the variant display
# This lets us know where to print variant values.
max_name_default_len = max(
color.clen(_fmt_name_and_default(variant))
for name, when_variants in variants_by_name.items()
for variants in when_variants.values()
for variant in variants
)
return max_name_default_len, variants_by_name
def _unconstrained_ver_first(item):
"""sort key that puts specs with open version ranges first"""
spec, _ = item
return (spack.version.any_version not in spec.versions, spec)
def print_variants_grouped_by_when(pkg):
max_name_default_len, _ = _print_variants_header(pkg)
indent = 4
variants = _variants_by_when_name(pkg)
for when, variants_by_name in sorted(variants.items(), key=_unconstrained_ver_first):
padded_values = max_name_default_len + 4
start_indent = indent
if when != spack.spec.Spec():
sys.stdout.write("\n")
sys.stdout.write(_fmt_when(when, indent))
sys.stdout.write("\n")
# indent names slightly inside 'when', but line up values
padded_values -= 2
start_indent += 2
for name, variant in sorted(variants_by_name.items()):
_fmt_variant(variant, padded_values, start_indent, None, out=sys.stdout)
def print_variants_by_name(pkg):
max_name_default_len, variants_by_name = _print_variants_header(pkg)
max_name_default_len += 4
indent = 4
for name, when_variants in variants_by_name.items():
for when, variants in sorted(when_variants.items(), key=_unconstrained_ver_first):
for variant in variants:
_fmt_variant(variant, max_name_default_len, indent, when, out=sys.stdout)
sys.stdout.write("\n")
def print_variants(pkg, args):
"""output variants"""
if args.variants_by_name:
print_variants_by_name(pkg)
else:
print_variants_grouped_by_when(pkg)
def print_versions(pkg, args):
"""output versions"""
color.cprint("")
color.cprint(section_title("Preferred version: "))
if not pkg.versions:
color.cprint(version(" None"))
color.cprint("")
color.cprint(section_title("Safe versions: "))
color.cprint(version(" None"))
color.cprint("")
color.cprint(section_title("Deprecated versions: "))
color.cprint(version(" None"))
else:
pad = padder(pkg.versions, 4)
preferred = preferred_version(pkg)
def get_url(version):
try:
return fs.for_package_version(pkg, version)
except spack.fetch_strategy.InvalidArgsError:
return "No URL"
url = get_url(preferred) if pkg.has_code else ""
line = version(" {0}".format(pad(preferred))) + color.cescape(url)
color.cwrite(line)
print()
safe = []
deprecated = []
for v in reversed(sorted(pkg.versions)):
if pkg.has_code:
url = get_url(v)
if pkg.versions[v].get("deprecated", False):
deprecated.append((v, url))
else:
safe.append((v, url))
for title, vers in [("Safe", safe), ("Deprecated", deprecated)]:
color.cprint("")
color.cprint(section_title("{0} versions: ".format(title)))
if not vers:
color.cprint(version(" None"))
continue
for v, url in vers:
line = version(" {0}".format(pad(v))) + color.cescape(url)
color.cprint(line)
def print_virtuals(pkg, args):
"""output virtual packages"""
color.cprint("")
color.cprint(section_title("Virtual Packages: "))
if pkg.provided:
inverse_map = {}
for spec, whens in pkg.provided.items():
for when in whens:
if when not in inverse_map:
inverse_map[when] = set()
inverse_map[when].add(spec)
for when, specs in reversed(sorted(inverse_map.items())):
line = " %s provides %s" % (
when.colorized(),
", ".join(s.colorized() for s in specs),
)
print(line)
else:
color.cprint(" None")
def print_licenses(pkg, args):
"""Output the licenses of the project."""
color.cprint("")
color.cprint(section_title("Licenses: "))
if len(pkg.licenses) == 0:
color.cprint(" None")
else:
pad = padder(pkg.licenses, 4)
for when_spec in pkg.licenses:
license_identifier = pkg.licenses[when_spec]
line = license(" {0}".format(pad(license_identifier))) + color.cescape(when_spec)
color.cprint(line)
def info(parser, args):
spec = spack.spec.Spec(args.package)
pkg_cls = spack.repo.PATH.get_pkg_class(spec.name)
pkg = pkg_cls(spec)
# Output core package information
header = section_title("{0}: ").format(pkg.build_system_class) + pkg.name
color.cprint(header)
color.cprint("")
color.cprint(section_title("Description:"))
if pkg.__doc__:
color.cprint(color.cescape(pkg.format_doc(indent=4)))
else:
color.cprint(" None")
if getattr(pkg, "homepage"):
color.cprint(section_title("Homepage: ") + pkg.homepage)
# Now output optional information in expected order
sections = [
(args.all or args.maintainers, print_maintainers),
(args.all or args.detectable, print_detectable),
(args.all or args.tags, print_tags),
(args.all or not args.no_versions, print_versions),
(args.all or not args.no_variants, print_variants),
(args.all or args.phases, print_phases),
(args.all or not args.no_dependencies, print_dependencies),
(args.all or args.virtuals, print_virtuals),
(args.all or args.tests, print_tests),
(args.all or True, print_licenses),
]
for print_it, func in sections:
if print_it:
func(pkg, args)
color.cprint("")