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 error import *
from package import Package from package import Package
from relations import depends_on, provides from relations import depends_on, provides, patch
from multimethod import when 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): def setup_parser(subparser):
subparser.add_argument( subparser.add_argument(
'-n', '--no-checksum', action='store_true', dest='no_checksum', '-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( subparser.add_argument(
'packages', nargs=argparse.REMAINDER, help="specs of packages to stage") 'packages', nargs=argparse.REMAINDER, help="specs of packages to stage")

View File

@ -55,6 +55,7 @@
from spack.util.lang import * from spack.util.lang import *
from spack.util.web import get_pages from spack.util.web import get_pages
from spack.util.environment import * from spack.util.environment import *
from spack.util.filesystem import touch
class Package(object): class Package(object):
@ -267,10 +268,11 @@ class SomePackage(Package):
p = Package() # Done for you by spack p = Package() # Done for you by spack
p.do_fetch() # called by spack commands in spack/cmd. p.do_fetch() # downloads tarball from a URL
p.do_stage() # see spack.stage.Stage docs. 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_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: 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. """ """Specs of conflicting packages, keyed by name. """
conflicted = {} conflicted = {}
"""Patches to apply to newly expanded source, if any."""
patches = {}
# #
# These are default values for instance variables. # 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. """Creates a stage directory and downloads the taball for this package.
Working directory will be set to the stage directory. 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: 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 " tty.die("Cannot fetch %s@%s safely; there is no checksum on file for this "
"version." % (self.name, self.version), "version." % (self.name, self.version),
@ -590,6 +598,9 @@ def do_fetch(self):
def do_stage(self): def do_stage(self):
"""Unpacks the fetched tarball, then changes into the expanded tarball """Unpacks the fetched tarball, then changes into the expanded tarball
directory.""" directory."""
if not self.spec.concrete:
raise ValueError("Can only stage concrete packages.")
self.do_fetch() self.do_fetch()
archive_dir = self.stage.expanded_archive_path archive_dir = self.stage.expanded_archive_path
@ -601,6 +612,52 @@ def do_stage(self):
self.stage.chdir_to_archive() 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): def do_install(self):
"""This class should call this version of the install method. """This class should call this version of the install method.
Package implementations should override install(). Package implementations should override install().
@ -616,7 +673,7 @@ def do_install(self):
if not self.ignore_dependencies: if not self.ignore_dependencies:
self.do_install_dependencies() self.do_install_dependencies()
self.do_stage() self.do_patch()
self.setup_install_environment() self.setup_install_environment()
# Add convenience commands to the package's module scope to # 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) 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): def filename_for_package_name(pkg_name):
"""Get the filename for the module we should load for a particular package. """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 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. of the standalone .py file.
""" """
validate_package_name(pkg_name) validate_package_name(pkg_name)
pkg_dir = dirname_for_package_name(pkg_name)
pkg_dir = new_path(spack.packages_path, pkg_name)
if os.path.isdir(pkg_dir): if os.path.isdir(pkg_dir):
init_file = new_path(pkg_dir, '__init__.py') 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
import spack.spec import spack.spec
import spack.error import spack.error
from spack.patch import Patch
from spack.spec import Spec, parse_anonymous_spec from spack.spec import Spec, parse_anonymous_spec
from spack.packages import packages_module from spack.packages import packages_module
from spack.util.lang import * from spack.util.lang import *
@ -110,16 +112,35 @@ def provides(*specs, **kwargs):
provided[provided_spec] = provider_spec provided[provided_spec] = provider_spec
"""Packages can declare conflicts with other packages. def patch(url_or_filename, **kwargs):
This can be as specific as you like: use regular spec syntax. """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): 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 # TODO: implement conflicts
pass pass
class RelationError(spack.error.SpackError): class RelationError(spack.error.SpackError):
"""This is raised when something is wrong with a package relation.""" """This is raised when something is wrong with a package relation."""
def __init__(self, relation, message): def __init__(self, relation, message):

View File

@ -57,6 +57,11 @@ def working_dir(dirname):
os.chdir(orig_dir) os.chdir(orig_dir)
def touch(path):
with closing(open(path, 'a')) as file:
os.utime(path, None)
def mkdirp(*paths): def mkdirp(*paths):
for path in paths: for path in paths:
if not os.path.exists(path): if not os.path.exists(path):