package : introduced InstallPhase, added decorators for prerequisites and sanity_checks of phases

This commit is contained in:
alalazo 2016-07-08 12:29:49 +02:00
parent a36f3764af
commit 8ed028e2d6
2 changed files with 133 additions and 85 deletions

View file

@ -35,10 +35,13 @@
""" """
import os import os
import re import re
import string
import textwrap import textwrap
import time import time
import glob import inspect
import string import functools
from StringIO import StringIO
from urlparse import urlparse
import llnl.util.tty as tty import llnl.util.tty as tty
import spack import spack
@ -52,26 +55,95 @@
import spack.repository import spack.repository
import spack.url import spack.url
import spack.util.web import spack.util.web
from urlparse import urlparse
from StringIO import StringIO
from llnl.util.filesystem import * from llnl.util.filesystem import *
from llnl.util.lang import * from llnl.util.lang import *
from llnl.util.link_tree import LinkTree from llnl.util.link_tree import LinkTree
from llnl.util.tty.log import log_output from llnl.util.tty.log import log_output
from spack import directory_layout
from spack.stage import Stage, ResourceStage, StageComposite from spack.stage import Stage, ResourceStage, StageComposite
from spack.util.compression import allowed_archive from spack.util.compression import allowed_archive
from spack.util.environment import dump_environment from spack.util.environment import dump_environment
from spack.util.executable import ProcessError, Executable, which from spack.util.executable import ProcessError, which
from spack.version import * from spack.version import *
from spack import directory_layout
from urlparse import urlparse
"""Allowed URL schemes for spack packages.""" """Allowed URL schemes for spack packages."""
_ALLOWED_URL_SCHEMES = ["http", "https", "ftp", "file", "git"] _ALLOWED_URL_SCHEMES = ["http", "https", "ftp", "file", "git"]
class Package(object): class InstallPhase(object):
"""Manages a single phase of the installation
This descriptor stores at creation time the name of the method it should search
for execution. The method is retrieved at get time, so that it can be overridden
by subclasses of whatever class declared the phases.
It also provides hooks to execute prerequisite and sanity checks.
"""
def __init__(self, name):
self.name = name
self.preconditions = []
self.sanity_checks = []
def __get__(self, instance, owner):
# The caller is a class that is trying to customize
# my behavior adding something
if instance is None:
return self
# If instance is there the caller wants to execute the
# install phase, thus return a properly set wrapper
phase = getattr(instance, self.name)
@functools.wraps(phase)
def phase_wrapper(spec, prefix):
# Execute phase pre-conditions,
# and give them the chance to fail
for check in self.preconditions:
check(instance)
# Do something sensible at some point
phase(spec, prefix)
# Execute phase sanity_checks,
# and give them the chance to fail
for check in self.sanity_checks:
check(instance)
return phase_wrapper
class PackageMeta(type):
"""Conveniently transforms attributes to permit extensible phases
Iterates over the attribute 'phase' and creates / updates private
InstallPhase attributes in the class that is being initialized
"""
phase_fmt = '_InstallPhase_{0}'
def __init__(cls, name, bases, attr_dict):
super(PackageMeta, cls).__init__(name, bases, attr_dict)
# Parse if phases is in attr dict, then set
# install phases wrappers
if 'phases' in attr_dict:
cls.phases = [PackageMeta.phase_fmt.format(name) for name in attr_dict['phases']]
for phase_name, callback_name in zip(cls.phases, attr_dict['phases']):
setattr(cls, phase_name, InstallPhase(callback_name))
def _transform_checks(check_name):
attr_name = PackageMeta.phase_fmt.format(check_name)
checks = getattr(cls, attr_name, None)
if checks:
for phase_name, funcs in checks.items():
phase = getattr(cls, PackageMeta.phase_fmt.format(phase_name))
getattr(phase, check_name).extend(funcs)
# TODO : this should delete the attribute, as it is just a placeholder
# TODO : to know what to do at class definition time. Clearing it is fine
# TODO : too, but it just leaves an empty dictionary in place
setattr(cls, attr_name, {})
# Preconditions
_transform_checks('preconditions')
# Sanity checks
_transform_checks('sanity_checks')
class PackageBase(object):
"""This is the superclass for all spack packages. """This is the superclass for all spack packages.
***The Package class*** ***The Package class***
@ -304,6 +376,7 @@ class SomePackage(Package):
clean() (some of them do this), and others to provide custom behavior. clean() (some of them do this), and others to provide custom behavior.
""" """
__metaclass__ = PackageMeta
# #
# These are default values for instance variables. # These are default values for instance variables.
# #
@ -410,7 +483,6 @@ def package_dir(self):
"""Return the directory where the package.py file lives.""" """Return the directory where the package.py file lives."""
return os.path.dirname(self.module.__file__) return os.path.dirname(self.module.__file__)
@property @property
def global_license_dir(self): def global_license_dir(self):
"""Returns the directory where global license files for all """Returns the directory where global license files for all
@ -875,7 +947,6 @@ def _resource_stage(self, resource):
resource_stage_folder = '-'.join(pieces) resource_stage_folder = '-'.join(pieces)
return resource_stage_folder return resource_stage_folder
_phases = ['install', 'create_spack_logs']
def do_install(self, def do_install(self,
keep_prefix=False, keep_prefix=False,
keep_stage=False, keep_stage=False,
@ -909,7 +980,7 @@ def do_install(self,
""" """
# FIXME : we need a better semantic # FIXME : we need a better semantic
if allowed_phases is None: if allowed_phases is None:
allowed_phases = self._phases allowed_phases = self.phases
if not self.spec.concrete: if not self.spec.concrete:
raise ValueError("Can only install concrete packages.") raise ValueError("Can only install concrete packages.")
@ -921,8 +992,8 @@ def do_install(self,
return return
# Ensure package is not already installed # Ensure package is not already installed
# FIXME : This should be a pre-requisite to a phase # FIXME : skip condition : if any is True skip the installation
if 'install' in self._phases and spack.install_layout.check_installed(self.spec): if 'install' in self.phases and spack.install_layout.check_installed(self.spec):
tty.msg("%s is already installed in %s" % (self.name, self.prefix)) tty.msg("%s is already installed in %s" % (self.name, self.prefix))
rec = spack.installed_db.get_record(self.spec) rec = spack.installed_db.get_record(self.spec)
if (not rec.explicit) and explicit: if (not rec.explicit) and explicit:
@ -994,15 +1065,9 @@ def build_process():
True): True):
dump_environment(env_path) dump_environment(env_path)
try: try:
for phase in filter(lambda x: x in allowed_phases, self._phases): for phase in filter(lambda x: x in allowed_phases, self.phases):
# TODO : Log to screen the various phases # TODO : Log to screen the various phases
action = getattr(self, phase) getattr(self, phase)(self.spec, self.prefix)
if getattr(action, 'preconditions', None):
action.preconditions()
action(self.spec, self.prefix)
if getattr(action, 'postconditions', None):
action.postconditions()
except AttributeError as e: except AttributeError as e:
# FIXME : improve error messages # FIXME : improve error messages
raise ProcessError(e.message, long_message='') raise ProcessError(e.message, long_message='')
@ -1012,11 +1077,6 @@ def build_process():
e.build_log = log_path e.build_log = log_path
raise e raise e
# Ensure that something was actually installed.
# FIXME : This should be executed after 'install' as a postcondition
# if 'install' in self._phases:
# self.sanity_check_prefix()
# Run post install hooks before build stage is removed. # Run post install hooks before build stage is removed.
spack.hooks.post_install(self) spack.hooks.post_install(self)
@ -1034,7 +1094,7 @@ def build_process():
# Create the install prefix and fork the build process. # Create the install prefix and fork the build process.
spack.install_layout.create_install_directory(self.spec) spack.install_layout.create_install_directory(self.spec)
except directory_layout.InstallDirectoryAlreadyExistsError: except directory_layout.InstallDirectoryAlreadyExistsError:
if 'install' in self._phases: if 'install' in self.phases:
# Abort install if install directory exists. # Abort install if install directory exists.
# But do NOT remove it (you'd be overwriting someon else's stuff) # But do NOT remove it (you'd be overwriting someon else's stuff)
tty.warn("Keeping existing install prefix in place.") tty.warn("Keeping existing install prefix in place.")
@ -1062,7 +1122,7 @@ def build_process():
# the database, so that we don't need to re-read from file. # the database, so that we don't need to re-read from file.
spack.installed_db.add(self.spec, self.prefix, explicit=explicit) spack.installed_db.add(self.spec, self.prefix, explicit=explicit)
def create_spack_logs(self, spec, prefix): def log(self, spec, prefix):
# Copy provenance into the install directory on success # Copy provenance into the install directory on success
log_install_path = spack.install_layout.build_log_path( log_install_path = spack.install_layout.build_log_path(
self.spec) self.spec)
@ -1241,13 +1301,6 @@ def setup_dependent_package(self, module, dependent_spec):
""" """
pass pass
def install(self, spec, prefix):
"""
Package implementations override this with their own configuration
"""
raise InstallError("Package %s provides no install method!" %
self.name)
def do_uninstall(self, force=False): def do_uninstall(self, force=False):
if not self.installed: if not self.installed:
raise InstallError(str(self.spec) + " is not installed.") raise InstallError(str(self.spec) + " is not installed.")
@ -1446,6 +1499,33 @@ def rpath_args(self):
""" """
return " ".join("-Wl,-rpath,%s" % p for p in self.rpath) return " ".join("-Wl,-rpath,%s" % p for p in self.rpath)
@classmethod
def _register_checks(cls, check_type, *args):
def _register_sanity_checks(func):
attr_name = PackageMeta.phase_fmt.format(check_type)
sanity_checks = getattr(cls, attr_name, {})
for item in args:
checks = sanity_checks.setdefault(item, [])
checks.append(func)
setattr(cls, attr_name, sanity_checks)
return func
return _register_sanity_checks
@classmethod
def precondition(cls, *args):
return cls._register_checks('preconditions', *args)
@classmethod
def sanity_check(cls, *args):
return cls._register_checks('sanity_checks', *args)
class Package(PackageBase):
phases = ['install', 'log']
# This will be used as a registration decorator in user
# packages, if need be
PackageBase.sanity_check('install')(PackageBase.sanity_check_prefix)
def install_dependency_symlinks(pkg, spec, prefix): def install_dependency_symlinks(pkg, spec, prefix):
"""Execute a dummy install and flatten dependencies""" """Execute a dummy install and flatten dependencies"""
@ -1548,53 +1628,16 @@ def _hms(seconds):
parts.append("%.2fs" % s) parts.append("%.2fs" % s)
return ' '.join(parts) return ' '.join(parts)
#class StagedPackage(Package): # FIXME : remove this after checking that set_executable works the same way
# """A Package subclass where the install() is split up into stages."""
# _phases = ['configure']
# def install_setup(self):
# """Creates an spack_setup.py script to configure the package later if we like."""
# raise InstallError("Package %s provides no install_setup() method!" % self.name)
#
# def install_configure(self):
# """Runs the configure process."""
# raise InstallError("Package %s provides no install_configure() method!" % self.name)
#
# def install_build(self):
# """Runs the build process."""
# raise InstallError("Package %s provides no install_build() method!" % self.name)
#
# def install_install(self):
# """Runs the install process."""
# raise InstallError("Package %s provides no install_install() method!" % self.name)
#
# def install(self, spec, prefix):
# if 'setup' in self._phases:
# self.install_setup()
#
# if 'configure' in self._phases:
# self.install_configure()
#
# if 'build' in self._phases:
# self.install_build()
#
# if 'install' in self._phases:
# self.install_install()
# else:
# # Create a dummy file so the build doesn't fail.
# # That way, the module file will also be created.
# with open(os.path.join(prefix, 'dummy'), 'w') as fout:
# pass
# stackoverflow.com/questions/12791997/how-do-you-do-a-simple-chmod-x-from-within-python # stackoverflow.com/questions/12791997/how-do-you-do-a-simple-chmod-x-from-within-python
def make_executable(path): #def make_executable(path):
mode = os.stat(path).st_mode # mode = os.stat(path).st_mode
mode |= (mode & 0o444) >> 2 # copy R bits to X # mode |= (mode & 0o444) >> 2 # copy R bits to X
os.chmod(path, mode) # os.chmod(path, mode)
class CMakePackage(PackageBase):
class CMakePackage(Package): phases = ['setup', 'configure', 'build', 'install', 'provenance']
_phases = ['configure', 'build', 'install', 'provenance']
def make_make(self): def make_make(self):
import multiprocessing import multiprocessing
@ -1614,6 +1657,7 @@ def configure_args(self):
def configure_env(self): def configure_env(self):
"""Returns package-specific environment under which the configure command should be run.""" """Returns package-specific environment under which the configure command should be run."""
# FIXME : Why not EnvironmentModules
return dict() return dict()
def spack_transitive_include_path(self): def spack_transitive_include_path(self):
@ -1622,7 +1666,7 @@ def spack_transitive_include_path(self):
for dep in os.environ['SPACK_DEPENDENCIES'].split(os.pathsep) for dep in os.environ['SPACK_DEPENDENCIES'].split(os.pathsep)
) )
def setup(self): def setup(self, spec, prefix):
cmd = [str(which('cmake'))] + \ cmd = [str(which('cmake'))] + \
spack.build_environment.get_std_cmake_args(self) + \ spack.build_environment.get_std_cmake_args(self) + \
['-DCMAKE_INSTALL_PREFIX=%s' % os.environ['SPACK_PREFIX'], ['-DCMAKE_INSTALL_PREFIX=%s' % os.environ['SPACK_PREFIX'],
@ -1674,10 +1718,10 @@ def cmdlist(str):
fout.write(' %s\n' % arg) fout.write(' %s\n' % arg)
fout.write('""") + sys.argv[1:]\n') fout.write('""") + sys.argv[1:]\n')
fout.write('\nproc = subprocess.Popen(cmd, env=env)\nproc.wait()\n') fout.write('\nproc = subprocess.Popen(cmd, env=env)\nproc.wait()\n')
make_executable(setup_fname) set_executable(setup_fname)
def configure(self): def configure(self, spec, prefix):
cmake = which('cmake') cmake = which('cmake')
with working_dir(self.build_directory, create=True): with working_dir(self.build_directory, create=True):
os.environ.update(self.configure_env()) os.environ.update(self.configure_env())
@ -1685,12 +1729,12 @@ def configure(self):
options = self.configure_args() + spack.build_environment.get_std_cmake_args(self) options = self.configure_args() + spack.build_environment.get_std_cmake_args(self)
cmake(self.source_directory, *options) cmake(self.source_directory, *options)
def build(self): def build(self, spec, prefix):
make = self.make_make() make = self.make_make()
with working_dir(self.build_directory, create=False): with working_dir(self.build_directory, create=False):
make() make()
def install(self): def install(self, spec, prefix):
make = self.make_make() make = self.make_make()
with working_dir(self.build_directory, create=False): with working_dir(self.build_directory, create=False):
make('install') make('install')

View file

@ -34,6 +34,10 @@ class Szip(Package):
version('2.1', '902f831bcefb69c6b635374424acbead') version('2.1', '902f831bcefb69c6b635374424acbead')
@Package.sanity_check('install')
def always_raise(self):
raise RuntimeError('Precondition not respected')
def install(self, spec, prefix): def install(self, spec, prefix):
configure('--prefix=%s' % prefix, configure('--prefix=%s' % prefix,
'--enable-production', '--enable-production',