diff options
author | Peter Scheibel <scheibel1@llnl.gov> | 2018-02-06 10:48:58 -0500 |
---|---|---|
committer | Todd Gamblin <tgamblin@llnl.gov> | 2018-03-20 00:29:54 -0700 |
commit | 2379ed54b9e8183bec8487c08d66e5f4c51eeb64 (patch) | |
tree | 3f104fa7e8ccaadaff837c62f447ffc7bf8365d1 | |
parent | db81d19ddd38914d8149d1a6d8b6bcd459b20c87 (diff) | |
download | spack-2379ed54b9e8183bec8487c08d66e5f4c51eeb64.tar.gz spack-2379ed54b9e8183bec8487c08d66e5f4c51eeb64.tar.bz2 spack-2379ed54b9e8183bec8487c08d66e5f4c51eeb64.tar.xz spack-2379ed54b9e8183bec8487c08d66e5f4c51eeb64.zip |
package_hash: add code to generate a hash for a package file
This will be included in the full hash of packages.
7 files changed, 301 insertions, 0 deletions
diff --git a/lib/spack/spack/test/package_hash.py b/lib/spack/spack/test/package_hash.py new file mode 100644 index 0000000000..77768bb648 --- /dev/null +++ b/lib/spack/spack/test/package_hash.py @@ -0,0 +1,84 @@ +############################################################################## +# Copyright (c) 2016, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# +# This file is part of Spack. +# Written by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. +# LLNL-CODE-647188 +# +# For details, see https://software.llnl.gov/spack +# Please also see the LICENSE file for our notice and the LGPL. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License (as published by +# the Free Software Foundation) version 2.1 dated February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and +# conditions of the GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +############################################################################## +from spack.util.package_hash import package_hash, package_content +from spack.spec import Spec + + +def test_hash(tmpdir, builtin_mock, config): + package_hash("hash-test1@1.2") + + +def test_different_variants(tmpdir, builtin_mock, config): + spec1 = Spec("hash-test1@1.2 +variantx") + spec2 = Spec("hash-test1@1.2 +varianty") + assert package_hash(spec1) == package_hash(spec2) + + +def test_all_same_but_name(tmpdir, builtin_mock, config): + spec1 = Spec("hash-test1@1.2") + spec2 = Spec("hash-test2@1.2") + compare_sans_name(True, spec1, spec2) + + spec1 = Spec("hash-test1@1.2 +varianty") + spec2 = Spec("hash-test2@1.2 +varianty") + compare_sans_name(True, spec1, spec2) + + +def test_all_same_but_archive_hash(tmpdir, builtin_mock, config): + """ + Archive hash is not intended to be reflected in Package hash. + """ + spec1 = Spec("hash-test1@1.3") + spec2 = Spec("hash-test2@1.3") + compare_sans_name(True, spec1, spec2) + + +def test_all_same_but_patch_contents(tmpdir, builtin_mock, config): + spec1 = Spec("hash-test1@1.1") + spec2 = Spec("hash-test2@1.1") + compare_sans_name(True, spec1, spec2) + + +def test_all_same_but_patches_to_apply(tmpdir, builtin_mock, config): + spec1 = Spec("hash-test1@1.4") + spec2 = Spec("hash-test2@1.4") + compare_sans_name(True, spec1, spec2) + + +def test_all_same_but_install(tmpdir, builtin_mock, config): + spec1 = Spec("hash-test1@1.5") + spec2 = Spec("hash-test2@1.5") + compare_sans_name(False, spec1, spec2) + + +def compare_sans_name(eq, spec1, spec2): + content1 = package_content(spec1) + content1 = content1.replace(spec1.package.__class__.__name__, '') + content2 = package_content(spec2) + content2 = content2.replace(spec2.package.__class__.__name__, '') + if eq: + assert content1 == content2 + else: + assert content1 != content2 diff --git a/lib/spack/spack/util/package_hash.py b/lib/spack/spack/util/package_hash.py new file mode 100644 index 0000000000..6362d9d7bb --- /dev/null +++ b/lib/spack/spack/util/package_hash.py @@ -0,0 +1,151 @@ +############################################################################## +# Copyright (c) 2016, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# +# This file is part of Spack. +# Written by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. +# LLNL-CODE-647188 +# +# For details, see https://software.llnl.gov/spack +# Please also see the LICENSE file for our notice and the LGPL. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License (as published by +# the Free Software Foundation) version 2.1 dated February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and +# conditions of the GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +############################################################################## +import spack +from spack import directives +from spack.error import SpackError +from spack.spec import Spec +from spack.util.naming import mod_to_class + +import ast +import hashlib + + +class RemoveDocstrings(ast.NodeTransformer): + """Transformer that removes docstrings from a Python AST.""" + def remove_docstring(self, node): + if node.body: + if isinstance(node.body[0], ast.Expr) and \ + isinstance(node.body[0].value, ast.Str): + node.body.pop(0) + + self.generic_visit(node) + return node + + def visit_FunctionDef(self, node): + return self.remove_docstring(node) + + def visit_ClassDef(self, node): + return self.remove_docstring(node) + + def visit_Module(self, node): + return self.remove_docstring(node) + + +class RemoveDirectives(ast.NodeTransformer): + """Remove Spack directives from a package AST.""" + def __init__(self, spec): + self.spec = spec + + def is_directive(self, node): + return (isinstance(node, ast.Expr) and + node.value and isinstance(node.value, ast.Call) and + node.value.func.id in directives.__all__) + + def is_spack_attr(self, node): + return (isinstance(node, ast.Assign) and + node.targets and isinstance(node.targets[0], ast.Name) and + node.targets[0].id in spack.Package.metadata_attrs) + + def visit_ClassDef(self, node): + if node.name == mod_to_class(self.spec.name): + node.body = [ + c for c in node.body + if (not self.is_directive(c) and not self.is_spack_attr(c))] + return node + + +class TagMultiMethods(ast.NodeVisitor): + """Tag @when-decorated methods in a spec.""" + def __init__(self, spec): + self.spec = spec + self.methods = {} + + def visit_FunctionDef(self, node): + nodes = self.methods.setdefault(node.name, []) + if node.decorator_list: + dec = node.decorator_list[0] + if isinstance(dec, ast.Call) and dec.func.id == 'when': + cond = dec.args[0].s + nodes.append((node, self.spec.satisfies(cond, strict=True))) + else: + nodes.append((node, None)) + + +class ResolveMultiMethods(ast.NodeTransformer): + """Remove methods which do not exist if their @when is not satisfied.""" + def __init__(self, methods): + self.methods = methods + + def resolve(self, node): + if node.name not in self.methods: + raise PackageHashError( + "Future traversal visited new node: %s" % node.name) + + result = None + for n, cond in self.methods[node.name]: + if cond: + return n + if cond is None: + result = n + return result + + def visit_FunctionDef(self, node): + if self.resolve(node) is node: + node.decorator_list = [] + return node + return None + + +def package_content(spec): + return ast.dump(package_ast(spec)) + + +def package_hash(spec, content=None): + if content is None: + content = package_content(spec) + return hashlib.sha256(content.encode('utf-8')).digest().lower() + + +def package_ast(spec): + spec = Spec(spec) + + filename = spack.repo.filename_for_package_name(spec.name) + with open(filename) as f: + text = f.read() + root = ast.parse(text) + + root = RemoveDocstrings().visit(root) + + RemoveDirectives(spec).visit(root) + + fmm = TagMultiMethods(spec) + fmm.visit(root) + + root = ResolveMultiMethods(fmm.methods).visit(root) + return root + + +class PackageHashError(SpackError): + """Raised for all errors encountered during package hashing.""" diff --git a/var/spack/repos/builtin.mock/packages/hash-test1/package.py b/var/spack/repos/builtin.mock/packages/hash-test1/package.py new file mode 100644 index 0000000000..60c7fbe32b --- /dev/null +++ b/var/spack/repos/builtin.mock/packages/hash-test1/package.py @@ -0,0 +1,34 @@ +from spack import * + +import os + + +class HashTest1(Package): + """Used to test package hashing + """ + + homepage = "http://www.hashtest1.org" + url = "http://www.hashtest1.org/downloads/hashtest1-1.1.tar.bz2" + + version('1.1', 'a' * 32) + version('1.2', 'b' * 32) + version('1.3', 'c' * 32) + version('1.4', 'd' * 32) + + patch('patch1.patch', when="@1.1") + patch('patch2.patch', when="@1.4") + + variant('variantx', default=False, description='Test variant X') + variant('varianty', default=False, description='Test variant Y') + + def setup_dependent_environment(self, spack_env, run_env, dependent_spec): + pass + + @when('@:1.4') + def install(self, spec, prefix): + print("install 1") + os.listdir(os.getcwd()) + + @when('@1.5') + def install(self, spec, prefix): + os.listdir(os.getcwd()) diff --git a/var/spack/repos/builtin.mock/packages/hash-test1/patch1.patch b/var/spack/repos/builtin.mock/packages/hash-test1/patch1.patch new file mode 100644 index 0000000000..a333c82dfd --- /dev/null +++ b/var/spack/repos/builtin.mock/packages/hash-test1/patch1.patch @@ -0,0 +1 @@ +the contents of patch 1 (not a valid diff, but sufficient for testing) diff --git a/var/spack/repos/builtin.mock/packages/hash-test1/patch2.patch b/var/spack/repos/builtin.mock/packages/hash-test1/patch2.patch new file mode 100644 index 0000000000..9e292a10fe --- /dev/null +++ b/var/spack/repos/builtin.mock/packages/hash-test1/patch2.patch @@ -0,0 +1 @@ +the contents of patch 2 (not a valid diff, but sufficient for testing) diff --git a/var/spack/repos/builtin.mock/packages/hash-test2/package.py b/var/spack/repos/builtin.mock/packages/hash-test2/package.py new file mode 100644 index 0000000000..3c4c2addf2 --- /dev/null +++ b/var/spack/repos/builtin.mock/packages/hash-test2/package.py @@ -0,0 +1,28 @@ +from spack import * + +import os + + +class HashTest2(Package): + """Used to test package hashing + """ + + homepage = "http://www.hashtest2.org" + url = "http://www.hashtest1.org/downloads/hashtest2-1.1.tar.bz2" + + version('1.1', 'a' * 32) + version('1.2', 'b' * 32) + version('1.3', 'c' * 31 + 'x') # Source hash differs from hash-test1@1.3 + version('1.4', 'd' * 32) + + patch('patch1.patch', when="@1.1") + + variant('variantx', default=False, description='Test variant X') + variant('varianty', default=False, description='Test variant Y') + + def setup_dependent_environment(self, spack_env, run_env, dependent_spec): + pass + + def install(self, spec, prefix): + print("install 1") + os.listdir(os.getcwd()) diff --git a/var/spack/repos/builtin.mock/packages/hash-test2/patch1.patch b/var/spack/repos/builtin.mock/packages/hash-test2/patch1.patch new file mode 100644 index 0000000000..bbfa868935 --- /dev/null +++ b/var/spack/repos/builtin.mock/packages/hash-test2/patch1.patch @@ -0,0 +1,2 @@ +the different contents of patch 1 (not a valid diff, but sufficient for testing, +and different from patch 1 of hash-test1) |