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.
This commit is contained in:
Todd Gamblin 2014-02-08 18:11:54 -08:00
parent 9a29aa8d03
commit b816f71f8c
8 changed files with 246 additions and 13 deletions

View file

@ -27,5 +27,5 @@
from error import *
from package import Package
from relations import depends_on, provides
from relations import depends_on, provides, patch
from multimethod import when

View file

@ -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()

View file

@ -33,7 +33,7 @@
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")

View file

@ -55,6 +55,7 @@
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 SomePackage(Package):
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 SomePackage(Package):
"""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 @@ def do_fetch(self):
"""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 @@ def do_fetch(self):
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 @@ def do_stage(self):
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 @@ def do_install(self):
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

View file

@ -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')

94
lib/spack/spack/patch.py Normal file
View file

@ -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

View file

@ -75,6 +75,8 @@ class Mpileaks(Package):
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
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.
"""
def conflicts(*specs):
# 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):

View file

@ -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):