# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import argparse
import io
from typing import List, Optional
import spack.cmd
import spack.environment as ev
import spack.llnl.util.tty as tty
import spack.spec
import spack.store
import spack.verify
import spack.verify_libraries
from spack.cmd.common import arguments
from spack.llnl.string import plural
from spack.llnl.util.filesystem import visit_directory_tree
description = "verify spack installations on disk"
section = "admin"
level = "long"
[docs]
def setup_parser(subparser: argparse.ArgumentParser):
sp = subparser.add_subparsers(metavar="SUBCOMMAND", dest="verify_command")
manifest_subparser = sp.add_parser(
"manifest", help=verify_manifest.__doc__, description=verify_manifest.__doc__
)
manifest_subparser.set_defaults(subparser=manifest_subparser)
manifest_subparser.add_argument(
"-l", "--local", action="store_true", help="verify only locally installed packages"
)
manifest_subparser.add_argument(
"-j", "--json", action="store_true", help="output json-formatted errors"
)
manifest_subparser.add_argument("-a", "--all", action="store_true", help="verify all packages")
manifest_subparser.add_argument(
"specs_or_files", nargs=argparse.REMAINDER, help="specs or files to verify"
)
manifest_sp_type = manifest_subparser.add_mutually_exclusive_group()
manifest_sp_type.add_argument(
"-s",
"--specs",
action="store_const",
const="specs",
dest="type",
default="specs",
help="treat entries as specs (default)",
)
manifest_sp_type.add_argument(
"-f",
"--files",
action="store_const",
const="files",
dest="type",
default="specs",
help="treat entries as absolute filenames\n\ncannot be used with '-a'",
)
libraries_subparser = sp.add_parser(
"libraries", help=verify_libraries.__doc__, description=verify_libraries.__doc__
)
libraries_subparser.set_defaults(subparser=libraries_subparser)
arguments.add_common_arguments(libraries_subparser, ["constraint"])
versions_subparser = sp.add_parser(
"versions", help=verify_versions.__doc__, description=verify_versions.__doc__
)
versions_subparser.set_defaults(subparser=versions_subparser)
arguments.add_common_arguments(versions_subparser, ["constraint"])
[docs]
def verify(parser, args):
cmd = args.verify_command
if cmd == "libraries":
return verify_libraries(args)
elif cmd == "manifest":
return verify_manifest(args)
elif cmd == "versions":
return verify_versions(args)
parser.error("invalid verify subcommand")
[docs]
def verify_versions(args):
"""Check that all versions of installed packages are known to Spack and non-deprecated.
Reports errors for any of the following:
1. Installed package not loadable from the repo
2. Installed package version not known by the package recipe
3. Installed package version deprecated in the package recipe
"""
specs = args.specs(installed=True)
msg_lines = _verify_version(specs)
if msg_lines:
tty.die("\n".join(msg_lines))
def _verify_version(specs):
"""Helper method for verify_versions."""
missing_package = []
unknown_version = []
deprecated_version = []
for spec in specs:
try:
pkg = spec.package
except Exception as e:
tty.debug(str(e))
missing_package.append(spec)
continue
if spec.version not in pkg.versions:
unknown_version.append(spec)
continue
if pkg.versions[spec.version].get("deprecated", False):
deprecated_version.append(spec)
msg_lines = []
if missing_package or unknown_version or deprecated_version:
errors = len(missing_package) + len(unknown_version) + len(deprecated_version)
msg_lines = [f"{errors} installed packages have unknown/deprecated versions\n"]
msg_lines += [
f" Cannot check version for {spec} at {spec.prefix}. Cannot load package."
for spec in missing_package
]
msg_lines += [
f" Spec {spec} at {spec.prefix} has version {spec.version} unknown to Spack."
for spec in unknown_version
]
msg_lines += [
f" Spec {spec} at {spec.prefix} has deprecated version {spec.version}."
for spec in deprecated_version
]
return msg_lines
[docs]
def verify_libraries(args):
"""verify that shared libraries of install packages can be located in rpaths (Linux only)"""
specs_from_db = [s for s in args.specs(installed=True) if not s.external]
tty.info(f"Checking {len(specs_from_db)} packages for shared library resolution")
errors = 0
for spec in specs_from_db:
try:
pkg = spec.package
except Exception:
tty.warn(f"Skipping {spec.cformat('{name}{@version}{/hash}')} due to missing package")
error_msg = _verify_libraries(spec, pkg.unresolved_libraries)
if error_msg is not None:
errors += 1
tty.error(error_msg)
if errors:
tty.error(f"Cannot resolve shared libraries in {plural(errors, 'package')}")
return 1
def _verify_libraries(spec: spack.spec.Spec, unresolved_libraries: List[str]) -> Optional[str]:
"""Go over the prefix of the installed spec and verify its shared libraries can be resolved."""
visitor = spack.verify_libraries.ResolveSharedElfLibDepsVisitor(
[*spack.verify_libraries.ALLOW_UNRESOLVED, *unresolved_libraries]
)
visit_directory_tree(spec.prefix, visitor)
if not visitor.problems:
return None
output = io.StringIO()
visitor.write(output, indent=4, brief=True)
message = output.getvalue().rstrip()
return f"{spec.cformat('{name}{@version}{/hash}')}: {spec.prefix}:\n{message}"
[docs]
def verify_manifest(args):
"""verify that install directories have not been modified since installation"""
local = args.local
if args.type == "files":
if args.all:
args.subparser.error("cannot use --all with --files")
for file in args.specs_or_files:
results = spack.verify.check_file_manifest(file)
if results.has_errors():
if args.json:
print(results.json_string())
else:
print(results)
return 0
else:
spec_args = spack.cmd.parse_specs(args.specs_or_files)
if args.all:
query = spack.store.STORE.db.query_local if local else spack.store.STORE.db.query
# construct spec list
if spec_args:
spec_list = spack.cmd.parse_specs(args.specs_or_files)
specs = []
for spec in spec_list:
specs += query(spec, installed=True)
else:
specs = query(installed=True)
elif args.specs_or_files:
# construct disambiguated spec list
env = ev.active_environment()
specs = list(map(lambda x: spack.cmd.disambiguate_spec(x, env, local=local), spec_args))
else:
args.subparser.error("use --all or specify specs to verify")
for spec in specs:
tty.debug("Verifying package %s")
results = spack.verify.check_spec_manifest(spec)
if results.has_errors():
if args.json:
print(results.json_string())
else:
tty.msg("In package %s" % spec.format("{name}/{hash:7}"))
print(results)
return 1
else:
tty.debug(results)