package: add a content_hash method for packages

This calculates a hash which depends on the complete content of the
package including sources and the associated `package.py` file.
This commit is contained in:
Peter Scheibel 2018-02-06 10:53:48 -05:00 committed by Todd Gamblin
parent e97c28e5b3
commit 3501f2f4b5
2 changed files with 56 additions and 0 deletions

View file

@ -33,9 +33,11 @@
rundown on spack and how it differs from homebrew, look at the rundown on spack and how it differs from homebrew, look at the
README. README.
""" """
import base64
import contextlib import contextlib
import copy import copy
import functools import functools
import hashlib
import inspect import inspect
import itertools import itertools
import os import os
@ -74,6 +76,7 @@
from spack.util.executable import which from spack.util.executable import which
from spack.stage import Stage, ResourceStage, StageComposite from spack.stage import Stage, ResourceStage, StageComposite
from spack.util.environment import dump_environment from spack.util.environment import dump_environment
from spack.util.package_hash import package_hash
from spack.version import Version from spack.version import Version
"""Allowed URL schemes for spack packages.""" """Allowed URL schemes for spack packages."""
@ -1161,6 +1164,28 @@ def patches_to_apply(self):
if self.spec.satisfies(spec)) if self.spec.satisfies(spec))
return sorted(patchesToApply, key=lambda p: p.path_or_url) return sorted(patchesToApply, key=lambda p: p.path_or_url)
def content_hash(self, content=None):
"""Create a hash based on the sources and logic used to build the
package. This includes the contents of all applied patches and the
contents of applicable functions in the package subclass."""
hashContent = list()
source_id = fs.for_package_version(self, self.version).source_id()
if not source_id:
# TODO? in cases where a digest or source_id isn't available,
# should this attempt to download the source and set one? This
# probably only happens for source repositories which are
# referenced by branch name rather than tag or commit ID.
message = 'Missing a source id for {s.name}@{s.version}'
tty.warn(message.format(s=self))
hashContent.append(''.encode('utf-8'))
else:
hashContent.append(source_id.encode('utf-8'))
hashContent.extend(':'.join((p.sha256, str(p.level))).encode('utf-8')
for p in self.patches_to_apply())
hashContent.append(package_hash(self.spec, content))
return base64.b32encode(
hashlib.sha256(bytes().join(sorted(hashContent))).digest()).lower()
@property @property
def namespace(self): def namespace(self):
namespace, dot, module = self.__module__.rpartition('.') namespace, dot, module = self.__module__.rpartition('.')

View file

@ -29,6 +29,7 @@
from spack.repository import Repo from spack.repository import Repo
from spack.util.naming import mod_to_class from spack.util.naming import mod_to_class
from spack.spec import Spec from spack.spec import Spec
from spack.util.package_hash import package_content
@pytest.mark.usefixtures('config', 'builtin_mock') @pytest.mark.usefixtures('config', 'builtin_mock')
@ -67,6 +68,36 @@ def test_package_class_names(self):
assert 'Pmgrcollective' == mod_to_class('PmgrCollective') assert 'Pmgrcollective' == mod_to_class('PmgrCollective')
assert '_3db' == mod_to_class('3db') assert '_3db' == mod_to_class('3db')
def test_content_hash_all_same_but_patch_contents(self):
spec1 = Spec("hash-test1@1.1")
spec2 = Spec("hash-test2@1.1")
content1 = package_content(spec1)
content1 = content1.replace(spec1.package.__class__.__name__, '')
content2 = package_content(spec2)
content2 = content2.replace(spec2.package.__class__.__name__, '')
assert spec1.package.content_hash(content=content1) != \
spec2.package.content_hash(content=content2)
def test_content_hash_different_variants(self):
spec1 = Spec("hash-test1@1.2 +variantx")
spec2 = Spec("hash-test2@1.2 ~variantx")
content1 = package_content(spec1)
content1 = content1.replace(spec1.package.__class__.__name__, '')
content2 = package_content(spec2)
content2 = content2.replace(spec2.package.__class__.__name__, '')
assert spec1.package.content_hash(content=content1) == \
spec2.package.content_hash(content=content2)
def test_all_same_but_archive_hash(self):
spec1 = Spec("hash-test1@1.3")
spec2 = Spec("hash-test2@1.3")
content1 = package_content(spec1)
content1 = content1.replace(spec1.package.__class__.__name__, '')
content2 = package_content(spec2)
content2 = content2.replace(spec2.package.__class__.__name__, '')
assert spec1.package.content_hash(content=content1) != \
spec2.package.content_hash(content=content2)
# Below tests target direct imports of spack packages from the # Below tests target direct imports of spack packages from the
# spack.pkg namespace # spack.pkg namespace
def test_import_package(self): def test_import_package(self):