commands: add spack pkg source
and spack pkg hash
To make it easier to see how package hashes change and how they are computed, add two commands: * `spack pkg source <spec>`: dumps source code for a package to the terminal * `spack pkg source --canonical <spec>`: dumps canonicalized source code for a package to the terminal. It strips comments, directives, and known-unused multimethods from the package. It is used to generate package hashes. * `spack pkg hash <spec>`: This gives the package hash for a particular spec. It is generated from the canonical source code for the spec. - [x] `add spack pkg source` and `spack pkg hash` - [x] add tests - [x] fix bug in multimethod resolution with boolean `@when` values Co-authored-by: Greg Becker <becker33@llnl.gov>
This commit is contained in:
parent
106ae7abe6
commit
a18a0e7a47
4 changed files with 147 additions and 8 deletions
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
from llnl.util.filesystem import working_dir
|
from llnl.util.filesystem import working_dir
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
import spack.cmd.common.arguments as arguments
|
import spack.cmd.common.arguments as arguments
|
||||||
import spack.paths
|
import spack.paths
|
||||||
import spack.repo
|
import spack.repo
|
||||||
|
import spack.util.package_hash as ph
|
||||||
from spack.util.executable import which
|
from spack.util.executable import which
|
||||||
|
|
||||||
description = "query packages associated with particular git revisions"
|
description = "query packages associated with particular git revisions"
|
||||||
|
@ -70,6 +72,15 @@ def setup_parser(subparser):
|
||||||
'rev2', nargs='?', default='HEAD',
|
'rev2', nargs='?', default='HEAD',
|
||||||
help="revision to compare to rev1 (default is HEAD)")
|
help="revision to compare to rev1 (default is HEAD)")
|
||||||
|
|
||||||
|
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'])
|
||||||
|
|
||||||
|
hash_parser = sp.add_parser('hash', help=pkg_hash.__doc__)
|
||||||
|
arguments.add_common_arguments(hash_parser, ['spec'])
|
||||||
|
|
||||||
|
|
||||||
def packages_path():
|
def packages_path():
|
||||||
"""Get the test repo if it is active, otherwise the builtin repo."""
|
"""Get the test repo if it is active, otherwise the builtin repo."""
|
||||||
|
@ -201,14 +212,49 @@ def pkg_changed(args):
|
||||||
colify(sorted(packages))
|
colify(sorted(packages))
|
||||||
|
|
||||||
|
|
||||||
|
def pkg_source(args):
|
||||||
|
"""dump source code for a package"""
|
||||||
|
specs = spack.cmd.parse_specs(args.spec, concretize=False)
|
||||||
|
if len(specs) != 1:
|
||||||
|
tty.die("spack pkg source 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) as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
if sys.stdout.isatty():
|
||||||
|
tty.msg(message)
|
||||||
|
sys.stdout.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
def pkg(parser, args):
|
def pkg(parser, args):
|
||||||
if not spack.cmd.spack_is_git_repo():
|
if not spack.cmd.spack_is_git_repo():
|
||||||
tty.die("This spack is not a git clone. Can't use 'spack pkg'")
|
tty.die("This spack is not a git clone. Can't use 'spack pkg'")
|
||||||
|
|
||||||
action = {'add': pkg_add,
|
action = {
|
||||||
'diff': pkg_diff,
|
'add': pkg_add,
|
||||||
'list': pkg_list,
|
'diff': pkg_diff,
|
||||||
'removed': pkg_removed,
|
'list': pkg_list,
|
||||||
'added': pkg_added,
|
'removed': pkg_removed,
|
||||||
'changed': pkg_changed}
|
'added': pkg_added,
|
||||||
|
'changed': pkg_changed,
|
||||||
|
'source': pkg_source,
|
||||||
|
'hash': pkg_hash,
|
||||||
|
}
|
||||||
action[args.pkg_command](args)
|
action[args.pkg_command](args)
|
||||||
|
|
|
@ -236,3 +236,63 @@ def test_pkg_fails_when_not_git_repo(monkeypatch):
|
||||||
monkeypatch.setattr(spack.cmd, 'spack_is_git_repo', lambda: False)
|
monkeypatch.setattr(spack.cmd, 'spack_is_git_repo', lambda: False)
|
||||||
with pytest.raises(spack.main.SpackCommandError):
|
with pytest.raises(spack.main.SpackCommandError):
|
||||||
pkg('added')
|
pkg('added')
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkg_source_requires_one_arg(mock_packages):
|
||||||
|
with pytest.raises(spack.main.SpackCommandError):
|
||||||
|
pkg("source", "a", "b")
|
||||||
|
|
||||||
|
with pytest.raises(spack.main.SpackCommandError):
|
||||||
|
pkg("source", "--canonical", "a", "b")
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkg_source(mock_packages):
|
||||||
|
fake_source = pkg("source", "fake")
|
||||||
|
|
||||||
|
fake_file = spack.repo.path.filename_for_package_name("fake")
|
||||||
|
with open(fake_file) as f:
|
||||||
|
contents = f.read()
|
||||||
|
assert fake_source == contents
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkg_canonical_source(mock_packages):
|
||||||
|
source = pkg("source", "multimethod")
|
||||||
|
assert "@when('@2.0')" in source
|
||||||
|
assert "Check that multimethods work with boolean values" in source
|
||||||
|
|
||||||
|
canonical_1 = pkg("source", "--canonical", "multimethod@1.0")
|
||||||
|
assert "@when" not in canonical_1
|
||||||
|
assert "should_not_be_reached by diamond inheritance test" not in canonical_1
|
||||||
|
assert "return 'base@1.0'" in canonical_1
|
||||||
|
assert "return 'base@2.0'" not in canonical_1
|
||||||
|
assert "return 'first_parent'" not in canonical_1
|
||||||
|
assert "'should_not_be_reached by diamond inheritance test'" not in canonical_1
|
||||||
|
|
||||||
|
canonical_2 = pkg("source", "--canonical", "multimethod@2.0")
|
||||||
|
assert "@when" not in canonical_2
|
||||||
|
assert "return 'base@1.0'" not in canonical_2
|
||||||
|
assert "return 'base@2.0'" in canonical_2
|
||||||
|
assert "return 'first_parent'" in canonical_2
|
||||||
|
assert "'should_not_be_reached by diamond inheritance test'" not in canonical_2
|
||||||
|
|
||||||
|
canonical_3 = pkg("source", "--canonical", "multimethod@3.0")
|
||||||
|
assert "@when" not in canonical_3
|
||||||
|
assert "return 'base@1.0'" not in canonical_3
|
||||||
|
assert "return 'base@2.0'" not in canonical_3
|
||||||
|
assert "return 'first_parent'" not in canonical_3
|
||||||
|
assert "'should_not_be_reached by diamond inheritance test'" not in canonical_3
|
||||||
|
|
||||||
|
canonical_4 = pkg("source", "--canonical", "multimethod@4.0")
|
||||||
|
assert "@when" not in canonical_4
|
||||||
|
assert "return 'base@1.0'" not in canonical_4
|
||||||
|
assert "return 'base@2.0'" not in canonical_4
|
||||||
|
assert "return 'first_parent'" not in canonical_4
|
||||||
|
assert "'should_not_be_reached by diamond inheritance test'" in canonical_4
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkg_hash(mock_packages):
|
||||||
|
output = pkg("hash", "a", "b").strip().split()
|
||||||
|
assert len(output) == 2 and all(len(elt) == 32 for elt in output)
|
||||||
|
|
||||||
|
output = pkg("hash", "multimethod").strip().split()
|
||||||
|
assert len(output) == 1 and all(len(elt) == 32 for elt in output)
|
||||||
|
|
|
@ -96,7 +96,22 @@ def visit_FunctionDef(self, func): # noqa
|
||||||
try:
|
try:
|
||||||
# evaluate spec condition for any when's
|
# evaluate spec condition for any when's
|
||||||
cond = dec.args[0].s
|
cond = dec.args[0].s
|
||||||
conditions.append(self.spec.satisfies(cond, strict=True))
|
|
||||||
|
# Boolean literals come through like this
|
||||||
|
if isinstance(cond, bool):
|
||||||
|
conditions.append(cond)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# otherwise try to make a spec
|
||||||
|
try:
|
||||||
|
cond_spec = spack.spec.Spec(cond)
|
||||||
|
except Exception:
|
||||||
|
# Spec parsing failed -- we don't know what this is.
|
||||||
|
conditions.append(None)
|
||||||
|
else:
|
||||||
|
# Check statically whether spec satisfies the condition
|
||||||
|
conditions.append(self.spec.satisfies(cond_spec, strict=True))
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# In this case the condition for the 'when' decorator is
|
# In this case the condition for the 'when' decorator is
|
||||||
# not a string literal (for example it may be a Python
|
# not a string literal (for example it may be a Python
|
||||||
|
|
|
@ -1444,7 +1444,7 @@ _spack_pkg() {
|
||||||
then
|
then
|
||||||
SPACK_COMPREPLY="-h --help"
|
SPACK_COMPREPLY="-h --help"
|
||||||
else
|
else
|
||||||
SPACK_COMPREPLY="add list diff added changed removed"
|
SPACK_COMPREPLY="add list diff added changed removed source hash"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1502,6 +1502,24 @@ _spack_pkg_removed() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_spack_pkg_source() {
|
||||||
|
if $list_options
|
||||||
|
then
|
||||||
|
SPACK_COMPREPLY="-h --help -c --canonical"
|
||||||
|
else
|
||||||
|
_all_packages
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_spack_pkg_hash() {
|
||||||
|
if $list_options
|
||||||
|
then
|
||||||
|
SPACK_COMPREPLY="-h --help"
|
||||||
|
else
|
||||||
|
_all_packages
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
_spack_providers() {
|
_spack_providers() {
|
||||||
if $list_options
|
if $list_options
|
||||||
then
|
then
|
||||||
|
|
Loading…
Reference in a new issue