summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorTodd Gamblin <tgamblin@llnl.gov>2014-02-08 18:11:54 -0800
committerTodd Gamblin <tgamblin@llnl.gov>2014-02-08 18:11:54 -0800
commitb816f71f8c8e7b39b5a385019d65d8a52f003463 (patch)
tree128c021f19ab5e387a0b2626f95ee6da7d33ca58 /lib
parent9a29aa8d032f1e5e1dd8cc2640233481f2bfd7a2 (diff)
downloadspack-b816f71f8c8e7b39b5a385019d65d8a52f003463.tar.gz
spack-b816f71f8c8e7b39b5a385019d65d8a52f003463.tar.bz2
spack-b816f71f8c8e7b39b5a385019d65d8a52f003463.tar.xz
spack-b816f71f8c8e7b39b5a385019d65d8a52f003463.zip
Support for patches in packages.
- packages can provide patch() directive to specify a patch file that should be applied to source code after expanding it and before building. - patches can have a when spec, so they're only applied under certain conditions - patches can be local files in the package's own directory, or they can be URLs which will be fetched from the internet.
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/__init__.py2
-rw-r--r--lib/spack/spack/cmd/patch.py51
-rw-r--r--lib/spack/spack/cmd/stage.py2
-rw-r--r--lib/spack/spack/package.py65
-rw-r--r--lib/spack/spack/packages/__init__.py9
-rw-r--r--lib/spack/spack/patch.py94
-rw-r--r--lib/spack/spack/relations.py31
-rw-r--r--lib/spack/spack/util/filesystem.py5
8 files changed, 246 insertions, 13 deletions
diff --git a/lib/spack/spack/__init__.py b/lib/spack/spack/__init__.py
index 911309fed3..77aad98524 100644
--- a/lib/spack/spack/__init__.py
+++ b/lib/spack/spack/__init__.py
@@ -27,5 +27,5 @@ from util import *
from error import *
from package import Package
-from relations import depends_on, provides
+from relations import depends_on, provides, patch
from multimethod import when
diff --git a/lib/spack/spack/cmd/patch.py b/lib/spack/spack/cmd/patch.py
new file mode 100644
index 0000000000..cc790df56e
--- /dev/null
+++ b/lib/spack/spack/cmd/patch.py
@@ -0,0 +1,51 @@
+##############################################################################
+# Copyright (c) 2013, 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://scalability-llnl.github.io/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 argparse
+
+import spack.cmd
+import spack.packages as packages
+
+
+description="Patch expanded archive sources in preparation for install"
+
+def setup_parser(subparser):
+ subparser.add_argument(
+ '-n', '--no-checksum', action='store_true', dest='no_checksum',
+ help="Do not check downloaded packages against checksum")
+ subparser.add_argument(
+ 'packages', nargs=argparse.REMAINDER, help="specs of packages to stage")
+
+
+def patch(parser, args):
+ if not args.packages:
+ tty.die("patch requires at least one package argument")
+
+ if args.no_checksum:
+ spack.do_checksum = False
+
+ specs = spack.cmd.parse_specs(args.packages, concretize=True)
+ for spec in specs:
+ package = packages.get(spec)
+ package.do_patch()
diff --git a/lib/spack/spack/cmd/stage.py b/lib/spack/spack/cmd/stage.py
index 594d1f727d..2ce3d66bcb 100644
--- a/lib/spack/spack/cmd/stage.py
+++ b/lib/spack/spack/cmd/stage.py
@@ -33,7 +33,7 @@ description="Expand downloaded archive in preparation for install"
def setup_parser(subparser):
subparser.add_argument(
'-n', '--no-checksum', action='store_true', dest='no_checksum',
- help="Do not check packages against checksum")
+ help="Do not check downloaded packages against checksum")
subparser.add_argument(
'packages', nargs=argparse.REMAINDER, help="specs of packages to stage")
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
index 7fe4d77b71..f733c5cbd2 100644
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -55,6 +55,7 @@ from spack.stage import Stage
from spack.util.lang import *
from spack.util.web import get_pages
from spack.util.environment import *
+from spack.util.filesystem import touch
class Package(object):
@@ -267,10 +268,11 @@ class Package(object):
p = Package() # Done for you by spack
- p.do_fetch() # called by spack commands in spack/cmd.
- p.do_stage() # see spack.stage.Stage docs.
+ p.do_fetch() # downloads tarball from a URL
+ p.do_stage() # expands tarball in a temp directory
+ p.do_patch() # applies patches to expanded source
p.do_install() # calls package's install() function
- p.do_uninstall()
+ p.do_uninstall() # removes install directory
There are also some other commands that clean the build area:
@@ -304,6 +306,9 @@ class Package(object):
"""Specs of conflicting packages, keyed by name. """
conflicted = {}
+ """Patches to apply to newly expanded source, if any."""
+ patches = {}
+
#
# These are default values for instance variables.
#
@@ -569,6 +574,9 @@ class Package(object):
"""Creates a stage directory and downloads the taball for this package.
Working directory will be set to the stage directory.
"""
+ if not self.spec.concrete:
+ raise ValueError("Can only fetch concrete packages.")
+
if spack.do_checksum and not self.version in self.versions:
tty.die("Cannot fetch %s@%s safely; there is no checksum on file for this "
"version." % (self.name, self.version),
@@ -590,6 +598,9 @@ class Package(object):
def do_stage(self):
"""Unpacks the fetched tarball, then changes into the expanded tarball
directory."""
+ if not self.spec.concrete:
+ raise ValueError("Can only stage concrete packages.")
+
self.do_fetch()
archive_dir = self.stage.expanded_archive_path
@@ -601,6 +612,52 @@ class Package(object):
self.stage.chdir_to_archive()
+ def do_patch(self):
+ """Calls do_stage(), then applied patches to the expanded tarball if they
+ haven't been applied already."""
+ if not self.spec.concrete:
+ raise ValueError("Can only patch concrete packages.")
+
+ self.do_stage()
+
+ # Construct paths to special files in the archive dir used to
+ # keep track of whether patches were successfully applied.
+ archive_dir = self.stage.expanded_archive_path
+ good_file = new_path(archive_dir, '.spack_patched')
+ bad_file = new_path(archive_dir, '.spack_patch_failed')
+
+ # If we encounter an archive that failed to patch, restage it
+ # so that we can apply all the patches again.
+ if os.path.isfile(bad_file):
+ tty.msg("Patching failed last time. Restaging.")
+ self.stage.restage()
+
+ self.stage.chdir_to_archive()
+
+ # If this file exists, then we already applied all the patches.
+ if os.path.isfile(good_file):
+ tty.msg("Already patched %s" % self.name)
+ return
+
+ # Apply all the patches for specs that match this on
+ for spec, patch_list in self.patches.items():
+ if self.spec.satisfies(spec):
+ for patch in patch_list:
+ tty.msg('Applying patch %s' % patch.path_or_url)
+ try:
+ patch.apply(self.stage)
+ except:
+ # Touch bad file if anything goes wrong.
+ touch(bad_file)
+ raise
+
+ # patch succeeded. Get rid of failed file & touch good file so we
+ # don't try to patch again again next time.
+ if os.path.isfile(bad_file):
+ os.remove(bad_file)
+ touch(good_file)
+
+
def do_install(self):
"""This class should call this version of the install method.
Package implementations should override install().
@@ -616,7 +673,7 @@ class Package(object):
if not self.ignore_dependencies:
self.do_install_dependencies()
- self.do_stage()
+ self.do_patch()
self.setup_install_environment()
# Add convenience commands to the package's module scope to
diff --git a/lib/spack/spack/packages/__init__.py b/lib/spack/spack/packages/__init__.py
index d9791ed1bc..488aea2423 100644
--- a/lib/spack/spack/packages/__init__.py
+++ b/lib/spack/spack/packages/__init__.py
@@ -209,6 +209,12 @@ def validate_package_name(pkg_name):
raise InvalidPackageNameError(pkg_name)
+def dirname_for_package_name(pkg_name):
+ """Get the directory name for a particular package would use, even if it's a
+ foo.py package and not a directory with a foo/__init__.py file."""
+ return new_path(spack.packages_path, pkg_name)
+
+
def filename_for_package_name(pkg_name):
"""Get the filename for the module we should load for a particular package.
The package can be either in a standalone .py file, or it can be in
@@ -227,8 +233,7 @@ def filename_for_package_name(pkg_name):
of the standalone .py file.
"""
validate_package_name(pkg_name)
-
- pkg_dir = new_path(spack.packages_path, pkg_name)
+ pkg_dir = dirname_for_package_name(pkg_name)
if os.path.isdir(pkg_dir):
init_file = new_path(pkg_dir, '__init__.py')
diff --git a/lib/spack/spack/patch.py b/lib/spack/spack/patch.py
new file mode 100644
index 0000000000..82a3a92449
--- /dev/null
+++ b/lib/spack/spack/patch.py
@@ -0,0 +1,94 @@
+##############################################################################
+# Copyright (c) 2013, 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://scalability-llnl.github.io/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 os
+
+import spack
+import spack.stage
+import spack.error
+import spack.packages as packages
+import spack.tty as tty
+
+from spack.util.executable import which
+from spack.util.filesystem import new_path
+
+# Patch tool for patching archives.
+_patch = which("patch", required=True)
+
+
+class Patch(object):
+ """This class describes a patch to be applied to some expanded
+ source code."""
+
+ def __init__(self, pkg_name, path_or_url, level):
+ self.pkg_name = pkg_name
+ self.path_or_url = path_or_url
+ self.path = None
+ self.url = None
+ self.level = level
+
+ if not isinstance(self.level, int) or not self.level >= 0:
+ raise ValueError("Patch level needs to be a non-negative integer.")
+
+ if '://' in path_or_url:
+ self.url = path_or_url
+ else:
+ pkg_dir = packages.dirname_for_package_name(pkg_name)
+ self.path = new_path(pkg_dir, path_or_url)
+ if not os.path.isfile(self.path):
+ raise NoSuchPatchFileError(pkg_name, self.path)
+
+
+ def apply(self, stage):
+ """Fetch this patch, if necessary, and apply it to the source
+ code in the supplied stage.
+ """
+ stage.chdir_to_archive()
+
+ patch_stage = None
+ try:
+ if self.url:
+ # use an anonymous stage to fetch the patch if it is a URL
+ patch_stage = spack.stage.Stage(self.url)
+ patch_stage.fetch()
+ patch_file = patch_stage.archive_file
+ else:
+ patch_file = self.path
+
+ # Use -N to allow the same patches to be applied multiple times.
+ _patch('-s', '-p', str(self.level), '-i', patch_file)
+
+ finally:
+ if patch_stage:
+ patch_stage.destroy()
+
+
+
+class NoSuchPatchFileError(spack.error.SpackError):
+ """Raised when user specifies a patch file that doesn't exist."""
+ def __init__(self, package, path):
+ super(NoSuchPatchFileError, self).__init__(
+ "No such patch file for package %s: %s" % (package, path))
+ self.package = package
+ self.path = path
diff --git a/lib/spack/spack/relations.py b/lib/spack/spack/relations.py
index 1c24db5fa6..28c9bf0363 100644
--- a/lib/spack/spack/relations.py
+++ b/lib/spack/spack/relations.py
@@ -75,6 +75,8 @@ import importlib
import spack
import spack.spec
import spack.error
+
+from spack.patch import Patch
from spack.spec import Spec, parse_anonymous_spec
from spack.packages import packages_module
from spack.util.lang import *
@@ -110,16 +112,35 @@ def provides(*specs, **kwargs):
provided[provided_spec] = provider_spec
-"""Packages can declare conflicts with other packages.
- This can be as specific as you like: use regular spec syntax.
-"""
+def patch(url_or_filename, **kwargs):
+ """Packages can declare patches to apply to source. You can
+ optionally provide a when spec to indicate that a particular
+ patch should only be applied when the package's spec meets
+ certain conditions (e.g. a particular version).
+ """
+ pkg = get_calling_package_name()
+ level = kwargs.get('level', 1)
+ when_spec = parse_anonymous_spec(kwargs.get('when', pkg), pkg)
+
+ patches = caller_locals().setdefault('patches', {})
+ if when_spec not in patches:
+ patches[when_spec] = [Patch(pkg, url_or_filename, level)]
+ else:
+ # if this spec is identical to some other, then append this
+ # patch to the existing list.
+ patches[when_spec].append(Patch(pkg, url_or_filename, level))
+
+
def conflicts(*specs):
+ """Packages can declare conflicts with other packages.
+ This can be as specific as you like: use regular spec syntax.
+
+ NOT YET IMPLEMENTED.
+ """
# TODO: implement conflicts
pass
-
-
class RelationError(spack.error.SpackError):
"""This is raised when something is wrong with a package relation."""
def __init__(self, relation, message):
diff --git a/lib/spack/spack/util/filesystem.py b/lib/spack/spack/util/filesystem.py
index d3c7b16457..c84a9fd608 100644
--- a/lib/spack/spack/util/filesystem.py
+++ b/lib/spack/spack/util/filesystem.py
@@ -57,6 +57,11 @@ def working_dir(dirname):
os.chdir(orig_dir)
+def touch(path):
+ with closing(open(path, 'a')) as file:
+ os.utime(path, None)
+
+
def mkdirp(*paths):
for path in paths:
if not os.path.exists(path):