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:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								lib/spack/spack/cmd/patch.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								lib/spack/spack/cmd/patch.py
									
									
									
									
									
										Normal 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()
 | 
			
		||||
@@ -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")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										94
									
								
								lib/spack/spack/patch.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
"""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):
 | 
			
		||||
 
 | 
			
		||||
@@ -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):
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user