# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import argparse
import os
import sys
import spack.cmd
import spack.llnl.util.tty as tty
import spack.repo
import spack.util.executable as exe
import spack.util.package_hash as ph
from spack.cmd.common import arguments
from spack.llnl.util.tty.colify import colify
description = "query packages associated with particular git revisions"
section = "developer"
level = "long"
[docs]
def setup_parser(subparser: argparse.ArgumentParser) -> None:
sp = subparser.add_subparsers(metavar="SUBCOMMAND", dest="pkg_command")
add_parser = sp.add_parser("add", help=pkg_add.__doc__)
arguments.add_common_arguments(add_parser, ["packages"])
add_parser.set_defaults(subparser=add_parser)
list_parser = sp.add_parser("list", help=pkg_list.__doc__)
list_parser.add_argument(
"rev", default="HEAD", nargs="?", help="revision to list packages for"
)
list_parser.set_defaults(subparser=list_parser)
diff_parser = sp.add_parser("diff", help=pkg_diff.__doc__)
diff_parser.add_argument(
"rev1", nargs="?", default="HEAD^", help="revision to compare against"
)
diff_parser.add_argument(
"rev2", nargs="?", default="HEAD", help="revision to compare to rev1 (default is HEAD)"
)
diff_parser.set_defaults(subparser=diff_parser)
add_parser = sp.add_parser("added", help=pkg_added.__doc__)
add_parser.add_argument("rev1", nargs="?", default="HEAD^", help="revision to compare against")
add_parser.add_argument(
"rev2", nargs="?", default="HEAD", help="revision to compare to rev1 (default is HEAD)"
)
add_parser.set_defaults(subparser=add_parser)
add_parser = sp.add_parser("changed", help=pkg_changed.__doc__)
add_parser.add_argument("rev1", nargs="?", default="HEAD^", help="revision to compare against")
add_parser.add_argument(
"rev2", nargs="?", default="HEAD", help="revision to compare to rev1 (default is HEAD)"
)
add_parser.add_argument(
"-t",
"--type",
action="store",
default="C",
help="types of changes to show (A: added, R: removed, C: changed); default is 'C'",
)
add_parser.set_defaults(subparser=add_parser)
rm_parser = sp.add_parser("removed", help=pkg_removed.__doc__)
rm_parser.add_argument("rev1", nargs="?", default="HEAD^", help="revision to compare against")
rm_parser.add_argument(
"rev2", nargs="?", default="HEAD", help="revision to compare to rev1 (default is HEAD)"
)
rm_parser.set_defaults(subparser=rm_parser)
# explicitly add help for `spack pkg grep` with just `--help` and NOT `-h`. This is so
# that the very commonly used -h (no filename) argument can be passed through to grep
grep_parser = sp.add_parser("grep", help=pkg_grep.__doc__, add_help=False)
grep_parser.add_argument(
"grep_args", nargs=argparse.REMAINDER, default=None, help="arguments for grep"
)
grep_parser.add_argument("--help", action="help", help="show this help message and exit")
grep_parser.set_defaults(subparser=grep_parser)
source_parser = sp.add_parser("source", help=pkg_source.__doc__)
source_parser.add_argument(
"-c",
"--canonical",
action="store_true",
default=False,
help="dump canonical source as used by package hash",
)
arguments.add_common_arguments(source_parser, ["spec"])
source_parser.set_defaults(subparser=source_parser)
hash_parser = sp.add_parser("hash", help=pkg_hash.__doc__)
arguments.add_common_arguments(hash_parser, ["spec"])
hash_parser.set_defaults(subparser=hash_parser)
[docs]
def pkg_add(args):
"""add a package to the git stage with ``git add``"""
spack.repo.add_package_to_git_stage(args.packages, spack.repo.builtin_repo())
[docs]
def pkg_list(args):
"""list packages associated with a particular spack git revision"""
colify(spack.repo.list_packages(args.rev, spack.repo.builtin_repo()))
[docs]
def pkg_diff(args):
"""compare packages available in two different git revisions"""
u1, u2 = spack.repo.diff_packages(args.rev1, args.rev2, spack.repo.builtin_repo())
if u1:
print("%s:" % args.rev1)
colify(sorted(u1), indent=4)
if u1:
print()
if u2:
print("%s:" % args.rev2)
colify(sorted(u2), indent=4)
[docs]
def pkg_removed(args):
"""show packages removed since a commit"""
u1, u2 = spack.repo.diff_packages(args.rev1, args.rev2, spack.repo.builtin_repo())
if u1:
colify(sorted(u1))
[docs]
def pkg_added(args):
"""show packages added since a commit"""
u1, u2 = spack.repo.diff_packages(args.rev1, args.rev2, spack.repo.builtin_repo())
if u2:
colify(sorted(u2))
[docs]
def pkg_changed(args):
"""show packages changed since a commit"""
packages = spack.repo.get_all_package_diffs(
args.type, spack.repo.builtin_repo(), args.rev1, args.rev2
)
if packages:
colify(sorted(packages))
[docs]
def pkg_source(args):
"""dump source code for a package"""
specs = spack.cmd.parse_specs(args.spec, concretize=False)
if len(specs) != 1:
args.subparser.error("requires exactly one spec")
spec = specs[0]
filename = spack.repo.PATH.filename_for_package_name(spec.name)
# regular source dump -- just get the package and print its contents
if args.canonical:
message = "Canonical source for %s:" % filename
content = ph.canonical_source(spec)
else:
message = "Source for %s:" % filename
with open(filename, encoding="utf-8") as f:
content = f.read()
if sys.stdout.isatty():
tty.msg(message)
sys.stdout.write(content)
[docs]
def pkg_hash(args):
"""dump canonical source code hash for a package spec"""
specs = spack.cmd.parse_specs(args.spec, concretize=False)
for spec in specs:
print(ph.package_hash(spec))
[docs]
def get_grep(required=False):
"""Get a grep command to use with ``spack pkg grep``."""
grep = exe.which(os.environ.get("SPACK_GREP") or "grep", required=required)
if grep:
grep.ignore_quotes = True # allow `spack pkg grep '"quoted string"'` without warning
return grep
[docs]
def pkg_grep(args, unknown_args):
"""grep for strings in package.py files from all repositories"""
grep = get_grep(required=True)
# add a little color to the output if we can
if "GNU" in grep("--version", output=str):
grep.add_default_arg("--color=auto")
all_paths = spack.repo.PATH.all_package_paths()
if not all_paths:
return 0 # no packages to search
# these args start every command invocation (grep arg1 arg2 ...)
all_prefix_args = grep.exe + args.grep_args + unknown_args
prefix_length = sum(spack.cmd.converted_arg_length(arg) for arg in all_prefix_args) + len(
all_prefix_args
)
# set up iterator and save the first group to ensure we don't end up with a group of size 1
groups = spack.cmd.group_arguments(all_paths, prefix_length=prefix_length)
# You can force GNU grep to show filenames on every line with -H, but not POSIX grep.
# POSIX grep only shows filenames when you're grepping 2 or more files. Since we
# don't know which one we're running, we ensure there are always >= 2 files by
# saving the prior group of paths and adding it to a straggling group of 1 if needed.
# This works unless somehow there is only one package in all of Spack.
prior_paths = next(groups)
# grep returns 1 for nothing found, 0 for something found, and > 1 for error
return_code = 1
# assemble args and run grep on a group of paths
def grep_group(paths):
all_args = args.grep_args + unknown_args + paths
grep(*all_args, fail_on_error=False)
return grep.returncode
for paths in groups:
if len(paths) == 1:
# Only the very last group can have length 1. If it does, combine
# it with the prior group to ensure more than one path is grepped.
prior_paths += paths
else:
# otherwise run grep on the prior group
error = grep_group(prior_paths)
if error != 1:
return_code = error
if error > 1: # fail fast on error
return error
prior_paths = paths
# Handle the last remaining group after the loop
error = grep_group(prior_paths)
if error != 1:
return_code = error
return return_code
[docs]
def pkg(parser, args, unknown_args):
if not spack.cmd.spack_is_git_repo():
tty.die("This spack is not a git clone. Can't use 'spack pkg'")
action = {
"add": pkg_add,
"added": pkg_added,
"changed": pkg_changed,
"diff": pkg_diff,
"hash": pkg_hash,
"list": pkg_list,
"removed": pkg_removed,
"source": pkg_source,
}
# grep is special as it passes unknown arguments through
if args.pkg_command == "grep":
return pkg_grep(args, unknown_args)
elif unknown_args:
args.subparser.error("unrecognized arguments: %s" % " ".join(unknown_args))
else:
return action[args.pkg_command](args)