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:
parent
9a29aa8d03
commit
b816f71f8c
8 changed files with 246 additions and 13 deletions
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue