summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/directives.py13
-rw-r--r--lib/spack/spack/package.py44
-rw-r--r--lib/spack/spack/patch.py279
-rw-r--r--lib/spack/spack/repo.py81
-rw-r--r--lib/spack/spack/spec.py22
-rw-r--r--lib/spack/spack/test/install.py7
-rw-r--r--lib/spack/spack/test/mirror.py4
-rw-r--r--lib/spack/spack/test/patch.py5
8 files changed, 339 insertions, 116 deletions
diff --git a/lib/spack/spack/directives.py b/lib/spack/spack/directives.py
index 57492f5c1c..5b2bf652ed 100644
--- a/lib/spack/spack/directives.py
+++ b/lib/spack/spack/directives.py
@@ -395,7 +395,7 @@ def patch(url_or_filename, level=1, when=None, working_dir=".", **kwargs):
certain conditions (e.g. a particular version).
Args:
- url_or_filename (str): url or filename of the patch
+ url_or_filename (str): url or relative filename of the patch
level (int): patch level (as in the patch shell command)
when (Spec): optional anonymous spec that specifies when to apply
the patch
@@ -417,13 +417,18 @@ def patch(url_or_filename, level=1, when=None, working_dir=".", **kwargs):
# patch to the existing list.
cur_patches = pkg_or_dep.patches.setdefault(when_spec, [])
- # if pkg_or_dep is a Dependency, make it a Package
pkg = pkg_or_dep
if isinstance(pkg, Dependency):
pkg = pkg.pkg
- cur_patches.append(spack.patch.create(
- pkg, url_or_filename, level, working_dir, **kwargs))
+ if '://' in url_or_filename:
+ patch = spack.patch.UrlPatch(
+ pkg, url_or_filename, level, working_dir, **kwargs)
+ else:
+ patch = spack.patch.FilePatch(
+ pkg, url_or_filename, level, working_dir)
+
+ cur_patches.append(patch)
return _execute_patch
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
index 7df500039e..4262ac14aa 100644
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -223,6 +223,11 @@ class PackageMeta(
namespace = namespace[len(prefix):]
return namespace
+ @property
+ def fullname(self):
+ """Name of this package, including the namespace"""
+ return '%s.%s' % (self.namespace, self.name)
+
def run_before(*phases):
"""Registers a method of a package to be run before a given phase"""
@@ -568,6 +573,11 @@ class PackageBase(with_metaclass(PackageMeta, PackageViewMixin, object)):
return type(self).namespace
@property
+ def fullname(self):
+ """Name of this package, including namespace: namespace.name."""
+ return type(self).fullname
+
+ @property
def global_license_dir(self):
"""Returns the directory where global license files for all
packages are stored."""
@@ -968,40 +978,6 @@ class PackageBase(with_metaclass(PackageMeta, PackageViewMixin, object)):
if not os.listdir(self.stage.path):
raise FetchError("Archive was empty for %s" % self.name)
- @classmethod
- def lookup_patch(cls, sha256):
- """Look up a patch associated with this package by its sha256 sum.
-
- Args:
- sha256 (str): sha256 sum of the patch to look up
-
- Returns:
- (Patch): ``Patch`` object with the given hash, or ``None`` if
- not found.
-
- To do the lookup, we build an index lazily. This allows us to
- avoid computing a sha256 for *every* patch and on every package
- load. With lazy hashing, we only compute hashes on lookup, which
- usually happens at build time.
-
- """
- if cls._patches_by_hash is None:
- cls._patches_by_hash = {}
-
- # Add patches from the class
- for cond, patch_list in cls.patches.items():
- for patch in patch_list:
- cls._patches_by_hash[patch.sha256] = patch
-
- # and patches on dependencies
- for name, conditions in cls.dependencies.items():
- for cond, dependency in conditions.items():
- for pcond, patch_list in dependency.patches.items():
- for patch in patch_list:
- cls._patches_by_hash[patch.sha256] = patch
-
- return cls._patches_by_hash.get(sha256, None)
-
def do_patch(self):
"""Applies patches if they haven't been applied already."""
if not self.spec.concrete:
diff --git a/lib/spack/spack/patch.py b/lib/spack/spack/patch.py
index 69949564c5..ec52a3705d 100644
--- a/lib/spack/spack/patch.py
+++ b/lib/spack/spack/patch.py
@@ -3,41 +3,24 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+import abc
+import hashlib
import os
import os.path
-import hashlib
+import six
import llnl.util.filesystem
+import llnl.util.lang
import spack.error
import spack.fetch_strategy as fs
+import spack.repo
import spack.stage
-from spack.util.crypto import checksum, Checker
+import spack.util.spack_json as sjson
-from spack.util.executable import which
from spack.util.compression import allowed_archive
-
-
-def create(pkg, path_or_url, level=1, working_dir=".", **kwargs):
- """Make either a FilePatch or a UrlPatch, depending on arguments.
-
- Args:
- pkg: package that needs to be patched
- path_or_url: path or url where the patch is found
- level: patch level (default 1)
- working_dir (str): relative path within the package stage;
- change to this before before applying (default '.')
-
- Returns:
- (Patch): a patch object on which ``apply(stage)`` can be called
- """
- # Check if we are dealing with a URL (which will be fetched)
- if '://' in path_or_url:
- return UrlPatch(path_or_url, level, working_dir, **kwargs)
-
- # If not, it's a file patch, which is stored within the repo directory.
- patch_path = os.path.join(pkg.package_dir, path_or_url)
- return FilePatch(patch_path, level, working_dir)
+from spack.util.crypto import checksum, Checker
+from spack.util.executable import which
def apply_patch(stage, patch_path, level=1, working_dir='.'):
@@ -58,54 +41,96 @@ def apply_patch(stage, patch_path, level=1, working_dir='.'):
'-d', working_dir)
+@six.add_metaclass(abc.ABCMeta)
class Patch(object):
"""Base class for patches.
- Defines the interface (basically just ``apply()``, at the moment) and
- common variables.
+ Arguments:
+ pkg (str): the package that owns the patch
+
+ The owning package is not necessarily the package to apply the patch
+ to -- in the case where a dependent package patches its dependency,
+ it is the dependent's fullname.
+
"""
- def __init__(self, path_or_url, level, working_dir):
+ def __init__(self, pkg, path_or_url, level, working_dir):
# validate level (must be an integer >= 0)
if not isinstance(level, int) or not level >= 0:
raise ValueError("Patch level needs to be a non-negative integer.")
# Attributes shared by all patch subclasses
+ self.owner = pkg.fullname
self.path_or_url = path_or_url # needed for debug output
self.level = level
self.working_dir = working_dir
- # path needs to be set by subclasses before calling self.apply()
- self.path = None
-
+ @abc.abstractmethod
def apply(self, stage):
- """Apply this patch to code in a stage."""
- assert self.path, "self.path must be set before Patch.apply()"
- apply_patch(stage, self.path, self.level, self.working_dir)
+ """Apply a patch to source in a stage.
+
+ Arguments:
+ stage (spack.stage.Stage): stage where source code lives
+ """
+ def to_dict(self):
+ """Partial dictionary -- subclases should add to this."""
+ return {
+ 'owner': self.owner,
+ 'sha256': self.sha256,
+ 'level': self.level,
+ 'working_dir': self.working_dir,
+ }
-class FilePatch(Patch):
- """Describes a patch that is retrieved from a file in the repository"""
- def __init__(self, path, level, working_dir):
- super(FilePatch, self).__init__(path, level, working_dir)
- if not os.path.isfile(path):
- raise NoSuchPatchError("No such patch: %s" % path)
- self.path = path
+class FilePatch(Patch):
+ """Describes a patch that is retrieved from a file in the repository.
+
+ Arguments:
+ pkg (str): the class object for the package that owns the patch
+ relative_path (str): path to patch, relative to the repository
+ directory for a package.
+ level (int): level to pass to patch command
+ working_dir (str): path within the source directory where patch
+ should be applied
+ """
+ def __init__(self, pkg, relative_path, level, working_dir):
+ self.relative_path = relative_path
+ self.path = os.path.join(pkg.package_dir, self.relative_path)
+ super(FilePatch, self).__init__(pkg, self.path, level, working_dir)
self._sha256 = None
+ def apply(self, stage):
+ if not os.path.isfile(self.path):
+ raise NoSuchPatchError("No such patch: %s" % self.path)
+ apply_patch(stage, self.path, self.level, self.working_dir)
+
@property
def sha256(self):
if self._sha256 is None:
self._sha256 = checksum(hashlib.sha256, self.path)
return self._sha256
+ def to_dict(self):
+ return llnl.util.lang.union_dicts(
+ super(FilePatch, self).to_dict(),
+ {'relative_path': self.relative_path})
+
class UrlPatch(Patch):
- """Describes a patch that is retrieved from a URL"""
- def __init__(self, url, level, working_dir, **kwargs):
- super(UrlPatch, self).__init__(url, level, working_dir)
+ """Describes a patch that is retrieved from a URL.
+
+ Arguments:
+ pkg (str): the package that owns the patch
+ url (str): URL where the patch can be fetched
+ level (int): level to pass to patch command
+ working_dir (str): path within the source directory where patch
+ should be applied
+ """
+ def __init__(self, pkg, url, level=1, working_dir='.', **kwargs):
+ super(UrlPatch, self).__init__(pkg, url, level, working_dir)
self.url = url
+ self.path = None # this is set in apply
self.archive_sha256 = kwargs.get('archive_sha256')
if allowed_archive(self.url) and not self.archive_sha256:
@@ -153,6 +178,7 @@ class UrlPatch(Patch):
raise NoSuchPatchError(
"Patch failed to download: %s" % self.url)
+ # set this here so that path is accessible after
self.path = os.path.join(root, files.pop())
if not os.path.isfile(self.path):
@@ -168,7 +194,170 @@ class UrlPatch(Patch):
"sha256 checksum failed for %s" % self.path,
"Expected %s but got %s" % (self.sha256, checker.sum))
- super(UrlPatch, self).apply(stage)
+ apply_patch(stage, self.path, self.level, self.working_dir)
+
+ def to_dict(self):
+ data = super(UrlPatch, self).to_dict()
+ data['url'] = self.url
+ if self.archive_sha256:
+ data['archive_sha256'] = self.archive_sha256
+ return data
+
+
+def from_dict(dictionary):
+ """Create a patch from json dictionary."""
+ owner = dictionary.get('owner')
+ if 'owner' not in dictionary:
+ raise ValueError('Invalid patch dictionary: %s' % dictionary)
+ pkg = spack.repo.get(owner)
+
+ if 'url' in dictionary:
+ return UrlPatch(
+ pkg,
+ dictionary['url'],
+ dictionary['level'],
+ dictionary['working_dir'],
+ sha256=dictionary['sha256'],
+ archive_sha256=dictionary.get('archive_sha256'))
+
+ elif 'relative_path' in dictionary:
+ patch = FilePatch(
+ pkg,
+ dictionary['relative_path'],
+ dictionary['level'],
+ dictionary['working_dir'])
+
+ # If the patch in the repo changes, we cannot get it back, so we
+ # just check it and fail here.
+ # TODO: handle this more gracefully.
+ sha256 = dictionary['sha256']
+ checker = Checker(sha256)
+ if not checker.check(patch.path):
+ raise fs.ChecksumError(
+ "sha256 checksum failed for %s" % patch.path,
+ "Expected %s but got %s" % (sha256, checker.sum),
+ "Patch may have changed since concretization.")
+ return patch
+ else:
+ raise ValueError("Invalid patch dictionary: %s" % dictionary)
+
+
+class PatchCache(object):
+ """Index of patches used in a repository, by sha256 hash.
+
+ This allows us to look up patches without loading all packages. It's
+ also needed to properly implement dependency patching, as need a way
+ to look up patches that come from packages not in the Spec sub-DAG.
+
+ The patch index is structured like this in a file (this is YAML, but
+ we write JSON)::
+
+ patches:
+ sha256:
+ namespace1.package1:
+ <patch json>
+ namespace2.package2:
+ <patch json>
+ ... etc. ...
+
+ """
+ def __init__(self, data=None):
+ if data is None:
+ self.index = {}
+ else:
+ if 'patches' not in data:
+ raise IndexError('invalid patch index; try `spack clean -m`')
+ self.index = data['patches']
+
+ @classmethod
+ def from_json(cls, stream):
+ return PatchCache(sjson.load(stream))
+
+ def to_json(self, stream):
+ sjson.dump({'patches': self.index}, stream)
+
+ def patch_for_package(self, sha256, pkg):
+ """Look up a patch in the index and build a patch object for it.
+
+ Arguments:
+ sha256 (str): sha256 hash to look up
+ pkg (spack.package.Package): Package object to get patch for.
+
+ We build patch objects lazily because building them requires that
+ we have information about the package's location in its repo.
+
+ """
+ sha_index = self.index.get(sha256)
+ if not sha_index:
+ raise NoSuchPatchError(
+ "Couldn't find patch with sha256: %s" % sha256)
+
+ patch_dict = sha_index.get(pkg.fullname)
+ if not patch_dict:
+ raise NoSuchPatchError(
+ "Couldn't find patch for package %s with sha256: %s"
+ % (pkg.fullname, sha256))
+
+ # add the sha256 back (we take it out on write to save space,
+ # because it's the index key)
+ patch_dict = dict(patch_dict)
+ patch_dict['sha256'] = sha256
+ return from_dict(patch_dict)
+
+ def update_package(self, pkg_fullname):
+ # remove this package from any patch entries that reference it.
+ empty = []
+ for sha256, package_to_patch in self.index.items():
+ remove = []
+ for fullname, patch_dict in package_to_patch.items():
+ if patch_dict['owner'] == pkg_fullname:
+ remove.append(fullname)
+
+ for fullname in remove:
+ package_to_patch.pop(fullname)
+
+ if not package_to_patch:
+ empty.append(sha256)
+
+ # remove any entries that are now empty
+ for sha256 in empty:
+ del self.index[sha256]
+
+ # update the index with per-package patch indexes
+ pkg = spack.repo.get(pkg_fullname)
+ partial_index = self._index_patches(pkg)
+ for sha256, package_to_patch in partial_index.items():
+ p2p = self.index.setdefault(sha256, {})
+ p2p.update(package_to_patch)
+
+ def update(self, other):
+ """Update this cache with the contents of another."""
+ for sha256, package_to_patch in other.index.items():
+ p2p = self.index.setdefault(sha256, {})
+ p2p.update(package_to_patch)
+
+ @staticmethod
+ def _index_patches(pkg_class):
+ index = {}
+
+ # Add patches from the class
+ for cond, patch_list in pkg_class.patches.items():
+ for patch in patch_list:
+ patch_dict = patch.to_dict()
+ patch_dict.pop('sha256') # save some space
+ index[patch.sha256] = {pkg_class.fullname: patch_dict}
+
+ # and patches on dependencies
+ for name, conditions in pkg_class.dependencies.items():
+ for cond, dependency in conditions.items():
+ for pcond, patch_list in dependency.patches.items():
+ for patch in patch_list:
+ dspec = spack.repo.get(dependency.spec.name)
+ patch_dict = patch.to_dict()
+ patch_dict.pop('sha256') # save some space
+ index[patch.sha256] = {dspec.fullname: patch_dict}
+
+ return index
class NoSuchPatchError(spack.error.SpackError):
diff --git a/lib/spack/spack/repo.py b/lib/spack/spack/repo.py
index 663edf2bf8..cc76c08416 100644
--- a/lib/spack/spack/repo.py
+++ b/lib/spack/spack/repo.py
@@ -13,7 +13,6 @@ import sys
import inspect
import re
import traceback
-import json
from contextlib import contextmanager
from six import string_types, add_metaclass
@@ -33,7 +32,9 @@ from llnl.util.filesystem import mkdirp, install
import spack.config
import spack.caches
import spack.error
+import spack.patch
import spack.spec
+import spack.util.spack_json as sjson
import spack.util.imp as simp
from spack.provider_index import ProviderIndex
from spack.util.path import canonicalize_path
@@ -190,11 +191,11 @@ class TagIndex(Mapping):
self._tag_dict = collections.defaultdict(list)
def to_json(self, stream):
- json.dump({'tags': self._tag_dict}, stream)
+ sjson.dump({'tags': self._tag_dict}, stream)
@staticmethod
def from_json(stream):
- d = json.load(stream)
+ d = sjson.load(stream)
r = TagIndex()
@@ -242,6 +243,22 @@ class Indexer(object):
def _create(self):
"""Create an empty index and return it."""
+ def needs_update(self, pkg):
+ """Whether an update is needed when the package file hasn't changed.
+
+ Returns:
+ (bool): ``True`` if this package needs its index
+ updated, ``False`` otherwise.
+
+ We already automatically update indexes when package files
+ change, but other files (like patches) may change underneath the
+ package file. This method can be used to check additional
+ package-specific files whenever they're loaded, to tell the
+ RepoIndex to update the index *just* for that package.
+
+ """
+ return False
+
@abc.abstractmethod
def read(self, stream):
"""Read this index from a provided file object."""
@@ -286,6 +303,28 @@ class ProviderIndexer(Indexer):
self.index.to_json(stream)
+class PatchIndexer(Indexer):
+ """Lifecycle methods for patch cache."""
+ def _create(self):
+ return spack.patch.PatchCache()
+
+ def needs_update():
+ # TODO: patches can change under a package and we should handle
+ # TODO: it, but we currently punt. This should be refactored to
+ # TODO: check whether patches changed each time a package loads,
+ # TODO: tell the RepoIndex to reindex them.
+ return False
+
+ def read(self, stream):
+ self.index = spack.patch.PatchCache.from_json(stream)
+
+ def write(self, stream):
+ self.index.to_json(stream)
+
+ def update(self, pkg_fullname):
+ self.index.update_package(pkg_fullname)
+
+
class RepoIndex(object):
"""Container class that manages a set of Indexers for a Repo.
@@ -403,6 +442,7 @@ class RepoPath(object):
self._all_package_names = None
self._provider_index = None
+ self._patch_index = None
# Add each repo to this path.
for repo in repos:
@@ -501,6 +541,16 @@ class RepoPath(object):
return self._provider_index
+ @property
+ def patch_index(self):
+ """Merged PatchIndex from all Repos in the RepoPath."""
+ if self._patch_index is None:
+ self._patch_index = spack.patch.PatchCache()
+ for repo in reversed(self.repos):
+ self._patch_index.update(repo.patch_index)
+
+ return self._patch_index
+
@_autospec
def providers_for(self, vpkg_spec):
providers = self.provider_index.providers_for(vpkg_spec)
@@ -870,15 +920,14 @@ class Repo(object):
"Repository %s does not contain package %s."
% (self.namespace, spec.fullname))
- # Install any patch files needed by packages.
+ # Install patch files needed by the package.
mkdirp(path)
- for spec, patches in spec.package.patches.items():
- for patch in patches:
- if patch.path:
- if os.path.exists(patch.path):
- install(patch.path, path)
- else:
- tty.warn("Patch file did not exist: %s" % patch.path)
+ for patch in spec.patches:
+ if patch.path:
+ if os.path.exists(patch.path):
+ install(patch.path, path)
+ else:
+ tty.warn("Patch file did not exist: %s" % patch.path)
# Install the package.py file itself.
install(self.filename_for_package_name(spec), path)
@@ -894,6 +943,7 @@ class Repo(object):
self._repo_index = RepoIndex(self._pkg_checker, self.namespace)
self._repo_index.add_indexer('providers', ProviderIndexer())
self._repo_index.add_indexer('tags', TagIndexer())
+ self._repo_index.add_indexer('patches', PatchIndexer())
return self._repo_index
@property
@@ -906,6 +956,11 @@ class Repo(object):
"""Index of tags and which packages they're defined on."""
return self.index['tags']
+ @property
+ def patch_index(self):
+ """Index of patches and packages they're defined on."""
+ return self.index['patches']
+
@_autospec
def providers_for(self, vpkg_spec):
providers = self.provider_index.providers_for(vpkg_spec)
@@ -1200,6 +1255,10 @@ class UnknownEntityError(RepoError):
"""Raised when we encounter a package spack doesn't have."""
+class IndexError(RepoError):
+ """Raised when there's an error with an index."""
+
+
class UnknownPackageError(UnknownEntityError):
"""Raised when we encounter a package spack doesn't have."""
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index 5563b94e3f..dee2f2e338 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -2677,24 +2677,16 @@ class Spec(object):
if 'patches' not in self.variants:
return []
- patches = []
-
- # FIXME: The private attribute below is attached after
+ # FIXME: _patches_in_order_of_appearance is attached after
# FIXME: concretization to store the order of patches somewhere.
# FIXME: Needs to be refactored in a cleaner way.
- for sha256 in self.variants['patches']._patches_in_order_of_appearance:
- patch = self.package_class.lookup_patch(sha256)
- if patch:
- patches.append(patch)
- continue
- # if not found in this package, check immediate dependents
- # for dependency patches
- for dep_spec in self._dependents.values():
- patch = dep_spec.parent.package_class.lookup_patch(sha256)
-
- if patch:
- patches.append(patch)
+ # translate patch sha256sums to patch objects by consulting the index
+ patches = []
+ for sha256 in self.variants['patches']._patches_in_order_of_appearance:
+ index = spack.repo.path.patch_index
+ patch = index.patch_for_package(sha256, self.package)
+ patches.append(patch)
return patches
diff --git a/lib/spack/spack/test/install.py b/lib/spack/spack/test/install.py
index 94b7ff51ff..85c860131a 100644
--- a/lib/spack/spack/test/install.py
+++ b/lib/spack/spack/test/install.py
@@ -101,13 +101,14 @@ def test_dont_add_patches_to_installed_package(install_mockery, mock_fetch):
dependency.concretize()
dependency.package.do_install()
- dependency.package.patches['dependency-install'] = [
- spack.patch.create(None, 'file://fake.patch', sha256='unused-hash')]
-
dependency_hash = dependency.dag_hash()
dependent = Spec('dependent-install ^/' + dependency_hash)
dependent.concretize()
+ dependency.package.patches['dependency-install'] = [
+ spack.patch.UrlPatch(
+ dependent.package, 'file://fake.patch', sha256='unused-hash')]
+
assert dependent['dependency-install'] == dependency
diff --git a/lib/spack/spack/test/mirror.py b/lib/spack/spack/test/mirror.py
index e2ddb85c97..a29454b4bd 100644
--- a/lib/spack/spack/test/mirror.py
+++ b/lib/spack/spack/test/mirror.py
@@ -160,7 +160,7 @@ def test_mirror_with_url_patches(mock_packages, config, monkeypatch):
with open(os.path.join(expanded_path, 'test.patch'), 'w'):
pass
- def successful_apply(_class, stage):
+ def successful_apply(*args, **kwargs):
pass
with Stage('spack-mirror-test') as stage:
@@ -170,7 +170,7 @@ def test_mirror_with_url_patches(mock_packages, config, monkeypatch):
successful_fetch)
monkeypatch.setattr(spack.fetch_strategy.URLFetchStrategy,
'expand', successful_expand)
- monkeypatch.setattr(spack.patch.Patch, 'apply', successful_apply)
+ monkeypatch.setattr(spack.patch, 'apply_patch', successful_apply)
monkeypatch.setattr(spack.caches.MirrorCache, 'store', record_store)
with spack.config.override('config:checksum', False):
diff --git a/lib/spack/spack/test/patch.py b/lib/spack/spack/test/patch.py
index 724924edf3..061d12b3bc 100644
--- a/lib/spack/spack/test/patch.py
+++ b/lib/spack/spack/test/patch.py
@@ -41,8 +41,9 @@ data_path = os.path.join(spack.paths.test_path, 'data', 'patch')
def test_url_patch(mock_stage, filename, sha256, archive_sha256):
# Make a patch object
url = 'file://' + filename
- patch = spack.patch.create(
- None, url, sha256=sha256, archive_sha256=archive_sha256)
+ pkg = spack.repo.get('patch')
+ patch = spack.patch.UrlPatch(
+ pkg, url, sha256=sha256, archive_sha256=archive_sha256)
# make a stage
with Stage(url) as stage: # TODO: url isn't used; maybe refactor Stage