From b816f71f8c8e7b39b5a385019d65d8a52f003463 Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Sat, 8 Feb 2014 18:11:54 -0800 Subject: [PATCH] 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. --- lib/spack/spack/__init__.py | 2 +- lib/spack/spack/cmd/patch.py | 51 +++++++++++++++ lib/spack/spack/cmd/stage.py | 2 +- lib/spack/spack/package.py | 65 +++++++++++++++++-- lib/spack/spack/packages/__init__.py | 9 ++- lib/spack/spack/patch.py | 94 ++++++++++++++++++++++++++++ lib/spack/spack/relations.py | 31 +++++++-- lib/spack/spack/util/filesystem.py | 5 ++ 8 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 lib/spack/spack/cmd/patch.py create mode 100644 lib/spack/spack/patch.py 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 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 @@ 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.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 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 @@ 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): 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):