Added feature: package extensions
- packages can be "extended" by others - allows extension to be symlinked into extendee's prefix. - used for python modules. - first module: py-setuptools
This commit is contained in:
@@ -24,7 +24,8 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
__all__ = ['set_install_permissions', 'install', 'expand_user', 'working_dir',
|
__all__ = ['set_install_permissions', 'install', 'expand_user', 'working_dir',
|
||||||
'touch', 'mkdirp', 'force_remove', 'join_path', 'ancestor',
|
'touch', 'mkdirp', 'force_remove', 'join_path', 'ancestor',
|
||||||
'can_access', 'filter_file', 'change_sed_delimiter', 'is_exe']
|
'can_access', 'filter_file', 'change_sed_delimiter', 'is_exe',
|
||||||
|
'check_link_tree', 'merge_link_tree', 'unmerge_link_tree']
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -222,3 +223,82 @@ def ancestor(dir, n=1):
|
|||||||
def can_access(file_name):
|
def can_access(file_name):
|
||||||
"""True if we have read/write access to the file."""
|
"""True if we have read/write access to the file."""
|
||||||
return os.access(file_name, os.R_OK|os.W_OK)
|
return os.access(file_name, os.R_OK|os.W_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def traverse_link_tree(src_root, dest_root, follow_nonexisting=True, **kwargs):
|
||||||
|
# Yield directories before or after their contents.
|
||||||
|
order = kwargs.get('order', 'pre')
|
||||||
|
if order not in ('pre', 'post'):
|
||||||
|
raise ValueError("Order must be 'pre' or 'post'.")
|
||||||
|
|
||||||
|
# List of relative paths to ignore under the src root.
|
||||||
|
ignore = kwargs.get('ignore', None)
|
||||||
|
if isinstance(ignore, basestring):
|
||||||
|
ignore = (ignore,)
|
||||||
|
|
||||||
|
for dirpath, dirnames, filenames in os.walk(src_root):
|
||||||
|
rel_path = dirpath[len(src_root):]
|
||||||
|
rel_path = rel_path.lstrip(os.path.sep)
|
||||||
|
dest_dirpath = os.path.join(dest_root, rel_path)
|
||||||
|
|
||||||
|
# Don't descend into ignored directories
|
||||||
|
if ignore and dest_dirpath in ignore:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Don't descend into dirs in dest that do not exist in src.
|
||||||
|
if not follow_nonexisting:
|
||||||
|
dirnames[:] = [
|
||||||
|
d for d in dirnames
|
||||||
|
if os.path.exists(os.path.join(dest_dirpath, d))]
|
||||||
|
|
||||||
|
# preorder yields directories before children
|
||||||
|
if order == 'pre':
|
||||||
|
yield (dirpath, dest_dirpath)
|
||||||
|
|
||||||
|
for name in filenames:
|
||||||
|
src_file = os.path.join(dirpath, name)
|
||||||
|
dest_file = os.path.join(dest_dirpath, name)
|
||||||
|
|
||||||
|
# Ignore particular paths inside the install root.
|
||||||
|
src_relpath = src_file[len(src_root):]
|
||||||
|
src_relpath = src_relpath.lstrip(os.path.sep)
|
||||||
|
if ignore and src_relpath in ignore:
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield (src_file, dest_file)
|
||||||
|
|
||||||
|
# postorder yields directories after children
|
||||||
|
if order == 'post':
|
||||||
|
yield (dirpath, dest_dirpath)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def check_link_tree(src_root, dest_root, **kwargs):
|
||||||
|
for src, dest in traverse_link_tree(src_root, dest_root, False, **kwargs):
|
||||||
|
if os.path.exists(dest) and not os.path.isdir(dest):
|
||||||
|
return dest
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def merge_link_tree(src_root, dest_root, **kwargs):
|
||||||
|
kwargs['order'] = 'pre'
|
||||||
|
for src, dest in traverse_link_tree(src_root, dest_root, **kwargs):
|
||||||
|
if os.path.isdir(src):
|
||||||
|
mkdirp(dest)
|
||||||
|
else:
|
||||||
|
assert(not os.path.exists(dest))
|
||||||
|
os.symlink(src, dest)
|
||||||
|
|
||||||
|
|
||||||
|
def unmerge_link_tree(src_root, dest_root, **kwargs):
|
||||||
|
kwargs['order'] = 'post'
|
||||||
|
for src, dest in traverse_link_tree(src_root, dest_root, **kwargs):
|
||||||
|
if os.path.isdir(dest):
|
||||||
|
if not os.listdir(dest):
|
||||||
|
# TODO: what if empty directories were present pre-merge?
|
||||||
|
shutil.rmtree(dest, ignore_errors=True)
|
||||||
|
|
||||||
|
elif os.path.exists(dest):
|
||||||
|
if not os.path.islink(dest):
|
||||||
|
raise ValueError("%s is not a link tree!" % dest)
|
||||||
|
os.remove(dest)
|
||||||
|
@@ -138,7 +138,7 @@
|
|||||||
# should live. This file is overloaded for spack core vs. for packages.
|
# should live. This file is overloaded for spack core vs. for packages.
|
||||||
#
|
#
|
||||||
__all__ = ['Package', 'Version', 'when', 'ver']
|
__all__ = ['Package', 'Version', 'when', 'ver']
|
||||||
from spack.package import Package
|
from spack.package import Package, ExtensionConflictError
|
||||||
from spack.version import Version, ver
|
from spack.version import Version, ver
|
||||||
from spack.multimethod import when
|
from spack.multimethod import when
|
||||||
|
|
||||||
|
@@ -53,6 +53,19 @@ def __init__(self, root):
|
|||||||
self.root = root
|
self.root = root
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hidden_file_paths(self):
|
||||||
|
"""Return a list of hidden files used by the directory layout.
|
||||||
|
|
||||||
|
Paths are relative to the root of an install directory.
|
||||||
|
|
||||||
|
If the directory layout uses no hidden files to maintain
|
||||||
|
state, this should return an empty container, e.g. [] or (,).
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def all_specs(self):
|
def all_specs(self):
|
||||||
"""To be implemented by subclasses to traverse all specs for which there is
|
"""To be implemented by subclasses to traverse all specs for which there is
|
||||||
a directory within the root.
|
a directory within the root.
|
||||||
@@ -156,6 +169,11 @@ def __init__(self, root, **kwargs):
|
|||||||
self.extension_file_name = extension_file_name
|
self.extension_file_name = extension_file_name
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hidden_file_paths(self):
|
||||||
|
return ('.spec', '.extensions')
|
||||||
|
|
||||||
|
|
||||||
def relative_path_for_spec(self, spec):
|
def relative_path_for_spec(self, spec):
|
||||||
_check_concrete(spec)
|
_check_concrete(spec)
|
||||||
dir_name = spec.format('$_$@$+$#')
|
dir_name = spec.format('$_$@$+$#')
|
||||||
@@ -249,28 +267,32 @@ def extension_file_path(self, spec):
|
|||||||
|
|
||||||
|
|
||||||
def get_extensions(self, spec):
|
def get_extensions(self, spec):
|
||||||
path = self.extension_file_path(spec)
|
_check_concrete(spec)
|
||||||
|
|
||||||
|
path = self.extension_file_path(spec)
|
||||||
extensions = set()
|
extensions = set()
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
with closing(open(path)) as spec_file:
|
with closing(open(path)) as ext_file:
|
||||||
for line in spec_file:
|
for line in ext_file:
|
||||||
try:
|
try:
|
||||||
extensions.add(Spec(line))
|
extensions.add(Spec(line.strip()))
|
||||||
except SpecError, e:
|
except spack.error.SpackError, e:
|
||||||
raise InvalidExtensionSpecError(str(e))
|
raise InvalidExtensionSpecError(str(e))
|
||||||
return extensions
|
return extensions
|
||||||
|
|
||||||
|
|
||||||
def write_extensions(self, extensions):
|
def write_extensions(self, spec, extensions):
|
||||||
path = self.extension_file_path(spec)
|
path = self.extension_file_path(spec)
|
||||||
with closing(open(path, 'w')) as spec_file:
|
with closing(open(path, 'w')) as spec_file:
|
||||||
for extension in sorted(extensions):
|
for extension in sorted(extensions):
|
||||||
spec_file.write("%s\n" % extensions)
|
spec_file.write("%s\n" % extension)
|
||||||
|
|
||||||
|
|
||||||
def add_extension(self, spec, extension_spec):
|
def add_extension(self, spec, extension_spec):
|
||||||
exts = get_extensions(spec)
|
_check_concrete(spec)
|
||||||
|
_check_concrete(extension_spec)
|
||||||
|
|
||||||
|
exts = self.get_extensions(spec)
|
||||||
if extension_spec in exts:
|
if extension_spec in exts:
|
||||||
raise ExtensionAlreadyInstalledError(spec, extension_spec)
|
raise ExtensionAlreadyInstalledError(spec, extension_spec)
|
||||||
else:
|
else:
|
||||||
@@ -279,16 +301,19 @@ def add_extension(self, spec, extension_spec):
|
|||||||
raise ExtensionConflictError(spec, extension_spec, already_installed)
|
raise ExtensionConflictError(spec, extension_spec, already_installed)
|
||||||
|
|
||||||
exts.add(extension_spec)
|
exts.add(extension_spec)
|
||||||
self.write_extensions(exts)
|
self.write_extensions(spec, exts)
|
||||||
|
|
||||||
|
|
||||||
def remove_extension(self, spec, extension_spec):
|
def remove_extension(self, spec, extension_spec):
|
||||||
exts = get_extensions(spec)
|
_check_concrete(spec)
|
||||||
|
_check_concrete(extension_spec)
|
||||||
|
|
||||||
|
exts = self.get_extensions(spec)
|
||||||
if not extension_spec in exts:
|
if not extension_spec in exts:
|
||||||
raise NoSuchExtensionError(spec, extension_spec)
|
raise NoSuchExtensionError(spec, extension_spec)
|
||||||
|
|
||||||
exts.remove(extension_spec)
|
exts.remove(extension_spec)
|
||||||
self.write_extensions(exts)
|
self.write_extensions(spec, exts)
|
||||||
|
|
||||||
|
|
||||||
class DirectoryLayoutError(SpackError):
|
class DirectoryLayoutError(SpackError):
|
||||||
@@ -328,7 +353,7 @@ class ExtensionAlreadyInstalledError(DirectoryLayoutError):
|
|||||||
"""Raised when an extension is added to a package that already has it."""
|
"""Raised when an extension is added to a package that already has it."""
|
||||||
def __init__(self, spec, extension_spec):
|
def __init__(self, spec, extension_spec):
|
||||||
super(ExtensionAlreadyInstalledError, self).__init__(
|
super(ExtensionAlreadyInstalledError, self).__init__(
|
||||||
"%s is already installed in %s" % (extension_spec, spec))
|
"%s is already installed in %s" % (extension_spec.short_spec, spec.short_spec))
|
||||||
|
|
||||||
|
|
||||||
class ExtensionConflictError(DirectoryLayoutError):
|
class ExtensionConflictError(DirectoryLayoutError):
|
||||||
@@ -336,12 +361,12 @@ class ExtensionConflictError(DirectoryLayoutError):
|
|||||||
def __init__(self, spec, extension_spec, conflict):
|
def __init__(self, spec, extension_spec, conflict):
|
||||||
super(ExtensionConflictError, self).__init__(
|
super(ExtensionConflictError, self).__init__(
|
||||||
"%s cannot be installed in %s because it conflicts with %s."% (
|
"%s cannot be installed in %s because it conflicts with %s."% (
|
||||||
extension_spec, spec, conflict))
|
extension_spec.short_spec, spec.short_spec, conflict.short_spec))
|
||||||
|
|
||||||
|
|
||||||
class NoSuchExtensionError(DirectoryLayoutError):
|
class NoSuchExtensionError(DirectoryLayoutError):
|
||||||
"""Raised when an extension isn't there on remove."""
|
"""Raised when an extension isn't there on remove."""
|
||||||
def __init__(self, spec, extension_spec):
|
def __init__(self, spec, extension_spec):
|
||||||
super(NoSuchExtensionError, self).__init__(
|
super(NoSuchExtensionError, self).__init__(
|
||||||
"%s cannot be removed from %s beacuse it's not installed."% (
|
"%s cannot be removed from %s because it's not installed."% (
|
||||||
extension_spec, spec, conflict))
|
extension_spec.short_spec, spec.short_spec))
|
||||||
|
49
lib/spack/spack/hooks/extensions.py
Normal file
49
lib/spack/spack/hooks/extensions.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
##############################################################################
|
||||||
|
# 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 spack
|
||||||
|
|
||||||
|
|
||||||
|
def post_install(pkg):
|
||||||
|
assert(pkg.spec.concrete)
|
||||||
|
for name, spec in pkg.extendees.items():
|
||||||
|
ext = pkg.spec[name]
|
||||||
|
epkg = ext.package
|
||||||
|
if epkg.installed:
|
||||||
|
epkg.do_activate(pkg)
|
||||||
|
|
||||||
|
|
||||||
|
def pre_uninstall(pkg):
|
||||||
|
assert(pkg.spec.concrete)
|
||||||
|
|
||||||
|
# Need to do this b/c uninstall does not automatically do it.
|
||||||
|
# TODO: store full graph info in stored .spec file.
|
||||||
|
pkg.spec.normalize()
|
||||||
|
|
||||||
|
for name, spec in pkg.extendees.items():
|
||||||
|
ext = pkg.spec[name]
|
||||||
|
epkg = ext.package
|
||||||
|
if epkg.installed:
|
||||||
|
epkg.do_deactivate(pkg)
|
@@ -329,6 +329,9 @@ class SomePackage(Package):
|
|||||||
"""By default we build in parallel. Subclasses can override this."""
|
"""By default we build in parallel. Subclasses can override this."""
|
||||||
parallel = True
|
parallel = True
|
||||||
|
|
||||||
|
"""Most packages are NOT extendable. Set to True if you want extensions."""
|
||||||
|
extendable = False
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, spec):
|
def __init__(self, spec):
|
||||||
# this determines how the package should be built.
|
# this determines how the package should be built.
|
||||||
@@ -398,6 +401,9 @@ def ensure_has_dict(attr_name):
|
|||||||
self._fetch_time = 0.0
|
self._fetch_time = 0.0
|
||||||
self._total_time = 0.0
|
self._total_time = 0.0
|
||||||
|
|
||||||
|
for name, spec in self.extendees.items():
|
||||||
|
spack.db.get(spec)._check_extendable()
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
@@ -877,6 +883,79 @@ def do_uninstall(self, **kwargs):
|
|||||||
spack.hooks.post_uninstall(self)
|
spack.hooks.post_uninstall(self)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_extendable(self):
|
||||||
|
if not self.extendable:
|
||||||
|
raise ValueError("Package %s is not extendable!" % self.name)
|
||||||
|
|
||||||
|
|
||||||
|
def _sanity_check_extension(self, extension):
|
||||||
|
self._check_extendable()
|
||||||
|
if not self.installed:
|
||||||
|
raise ValueError("Can only (de)activate extensions for installed packages.")
|
||||||
|
if not extension.installed:
|
||||||
|
raise ValueError("Extensions must first be installed.")
|
||||||
|
if not self.name in extension.extendees:
|
||||||
|
raise ValueError("%s does not extend %s!" % (extension.name, self.name))
|
||||||
|
if not self.spec.satisfies(extension.extendees[self.name]):
|
||||||
|
raise ValueError("%s does not satisfy %s!" % (self.spec, extension.spec))
|
||||||
|
|
||||||
|
|
||||||
|
def do_activate(self, extension):
|
||||||
|
self._sanity_check_extension(extension)
|
||||||
|
|
||||||
|
self.activate(extension)
|
||||||
|
spack.install_layout.add_extension(self.spec, extension.spec)
|
||||||
|
tty.msg("Activated extension %s for %s."
|
||||||
|
% (extension.spec.short_spec, self.spec.short_spec))
|
||||||
|
|
||||||
|
|
||||||
|
def activate(self, extension):
|
||||||
|
"""Symlinks all files from the extension into extendee's install dir.
|
||||||
|
|
||||||
|
Package authors can override this method to support other
|
||||||
|
extension mechanisms. Spack internals (commands, hooks, etc.)
|
||||||
|
should call do_activate() method so that proper checks are
|
||||||
|
always executed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
conflict = check_link_tree(
|
||||||
|
extension.prefix, self.prefix,
|
||||||
|
ignore=spack.install_layout.hidden_file_paths)
|
||||||
|
|
||||||
|
if conflict:
|
||||||
|
raise ExtensionConflictError(conflict)
|
||||||
|
|
||||||
|
merge_link_tree(extension.prefix, self.prefix,
|
||||||
|
ignore=spack.install_layout.hidden_file_paths)
|
||||||
|
|
||||||
|
|
||||||
|
def do_deactivate(self, extension):
|
||||||
|
self._sanity_check_extension(extension)
|
||||||
|
self.deactivate(extension)
|
||||||
|
|
||||||
|
ext = extension.spec
|
||||||
|
if ext in spack.install_layout.get_extensions(self.spec):
|
||||||
|
spack.install_layout.remove_extension(self.spec, ext)
|
||||||
|
|
||||||
|
tty.msg("Deactivated extension %s for %s."
|
||||||
|
% (extension.spec.short_spec, self.spec.short_spec))
|
||||||
|
|
||||||
|
|
||||||
|
def deactivate(self, extension):
|
||||||
|
"""Unlinks all files from extension out of extendee's install dir.
|
||||||
|
|
||||||
|
Package authors can override this method to support other
|
||||||
|
extension mechanisms. Spack internals (commands, hooks, etc.)
|
||||||
|
should call do_deactivate() method so that proper checks are
|
||||||
|
always executed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
unmerge_link_tree(extension.prefix, self.prefix,
|
||||||
|
ignore=spack.install_layout.hidden_file_paths)
|
||||||
|
tty.msg("Deactivated %s as extension of %s."
|
||||||
|
% (extension.spec.short_spec, self.spec.short_spec))
|
||||||
|
|
||||||
|
|
||||||
def do_clean(self):
|
def do_clean(self):
|
||||||
if self.stage.expanded_archive_path:
|
if self.stage.expanded_archive_path:
|
||||||
self.stage.chdir_to_source()
|
self.stage.chdir_to_source()
|
||||||
@@ -1068,3 +1147,9 @@ class NoURLError(PackageError):
|
|||||||
def __init__(self, cls):
|
def __init__(self, cls):
|
||||||
super(NoURLError, self).__init__(
|
super(NoURLError, self).__init__(
|
||||||
"Package %s has no version with a URL." % cls.__name__)
|
"Package %s has no version with a URL." % cls.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionConflictError(PackageError):
|
||||||
|
def __init__(self, path):
|
||||||
|
super(ExtensionConflictError, self).__init__(
|
||||||
|
"Extension blocked by file: %s" % path)
|
||||||
|
@@ -68,7 +68,7 @@ class Mpileaks(Package):
|
|||||||
spack install mpileaks ^mvapich
|
spack install mpileaks ^mvapich
|
||||||
spack install mpileaks ^mpich
|
spack install mpileaks ^mpich
|
||||||
"""
|
"""
|
||||||
__all__ = [ 'depends_on', 'provides', 'patch', 'version' ]
|
__all__ = [ 'depends_on', 'extends', 'provides', 'patch', 'version' ]
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import inspect
|
import inspect
|
||||||
@@ -135,7 +135,7 @@ def extends(*specs):
|
|||||||
for string in specs:
|
for string in specs:
|
||||||
for spec in spack.spec.parse(string):
|
for spec in spack.spec.parse(string):
|
||||||
if pkg == spec.name:
|
if pkg == spec.name:
|
||||||
raise CircularReferenceError('depends_on', pkg)
|
raise CircularReferenceError('extends', pkg)
|
||||||
dependencies[spec.name] = spec
|
dependencies[spec.name] = spec
|
||||||
extendees[spec.name] = spec
|
extendees[spec.name] = spec
|
||||||
|
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
from spack import *
|
from spack import *
|
||||||
|
|
||||||
|
|
||||||
class Python(Package):
|
class Python(Package):
|
||||||
"""The Python programming language."""
|
"""The Python programming language."""
|
||||||
homepage = "http://www.python.org"
|
homepage = "http://www.python.org"
|
||||||
url = "http://www.python.org/ftp/python/2.7.8/Python-2.7.8.tar.xz"
|
url = "http://www.python.org/ftp/python/2.7.8/Python-2.7.8.tar.xz"
|
||||||
|
|
||||||
|
extendable = True
|
||||||
|
|
||||||
version('2.7.8', 'd235bdfa75b8396942e360a70487ee00')
|
version('2.7.8', 'd235bdfa75b8396942e360a70487ee00')
|
||||||
|
|
||||||
depends_on("openssl")
|
depends_on("openssl")
|
||||||
|
Reference in New Issue
Block a user