Fix for SPACK-10: Spack now forks before install()

- this allows each install to have full control over its environment,
  and over spack.

- build process can do whatever it wants and doesn't affect main Spack
  process.
This commit is contained in:
Todd Gamblin 2014-04-16 00:53:20 -07:00
parent 554ae9b355
commit b4fddad7ef
3 changed files with 90 additions and 35 deletions

View File

@ -86,8 +86,11 @@ def remove_path_for_spec(self, spec):
shutil.rmtree(path, True) shutil.rmtree(path, True)
path = os.path.dirname(path) path = os.path.dirname(path)
while not os.listdir(path) and path != self.root: while path != self.root:
os.rmdir(path) if os.path.isdir(path):
if os.listdir(path):
return
os.rmdir(path)
path = os.path.dirname(path) path = os.path.dirname(path)

View File

@ -33,9 +33,9 @@
rundown on spack and how it differs from homebrew, look at the rundown on spack and how it differs from homebrew, look at the
README. README.
""" """
import inspect
import os import os
import re import re
import inspect
import subprocess import subprocess
import platform as py_platform import platform as py_platform
import multiprocessing import multiprocessing
@ -387,8 +387,9 @@ def default_version(self):
try: try:
return url.parse_version(self.__class__.url) return url.parse_version(self.__class__.url)
except UndetectableVersionError: except UndetectableVersionError:
tty.die("Couldn't extract a default version from %s. You " + raise PackageError(
"must specify it explicitly in the package." % self.url) "Couldn't extract a default version from %s." % self.url,
" You must specify it explicitly in the package file.")
@property @property
@ -490,7 +491,7 @@ def virtual_dependencies(self, visited=None):
@property @property
def installed(self): def installed(self):
return os.path.exists(self.prefix) return os.path.isdir(self.prefix)
@property @property
@ -544,10 +545,11 @@ def do_fetch(self):
raise ValueError("Can only fetch concrete packages.") 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 " raise ChecksumError(
"version." % (self.name, self.version), "Cannot fetch %s safely; there is no checksum on file for version %s."
"Add a checksum to the package file, or use --no-checksum to " % self.version,
"skip this check.") "Add a checksum to the package file, or use --no-checksum to "
"skip this check.")
self.stage.fetch() self.stage.fetch()
@ -557,8 +559,9 @@ def do_fetch(self):
if checker.check(self.stage.archive_file): if checker.check(self.stage.archive_file):
tty.msg("Checksum passed for %s" % self.name) tty.msg("Checksum passed for %s" % self.name)
else: else:
tty.die("%s checksum failed for %s. Expected %s but got %s." raise ChecksumError(
% (checker.hash_name, self.name, digest, checker.sum)) "%s checksum failed for %s." % checker.hash_name,
"Expected %s but got %s." % (self.name, digest, checker.sum))
def do_stage(self): def do_stage(self):
@ -640,32 +643,56 @@ def do_install(self):
self.do_patch() self.do_patch()
# create the install directory (allow the layout to handle this in # Fork a child process to do the build. This allows each
# case it needs to add extra files) # package authors to have full control over their environment,
spack.install_layout.make_path_for_spec(self.spec) # etc. without offecting other builds that might be executed
# in the same spack call.
tty.msg("Building %s." % self.name)
try: try:
pid = os.fork()
except OSError, e:
raise InstallError("Unable to fork build process: %s" % e)
if pid == 0:
tty.msg("Building %s." % self.name)
# create the install directory (allow the layout to handle
# this in case it needs to add extra files)
spack.install_layout.make_path_for_spec(self.spec)
# Set up process's build environment before running install.
build_env.set_build_environment_variables(self) build_env.set_build_environment_variables(self)
build_env.set_module_variables_for_package(self) build_env.set_module_variables_for_package(self)
self.install(self.spec, self.prefix) try:
if not os.path.isdir(self.prefix): # Subclasses implement install() to do the build &
tty.die("Install failed for %s. No install dir created." % self.name) # install work.
self.install(self.spec, self.prefix)
tty.msg("Successfully installed %s" % self.name) if not os.listdir(self.prefix):
print_pkg(self.prefix) raise InstallError(
"Install failed for %s. Nothing was installed!"
% self.name)
except Exception, e: # On successful install, remove the stage.
self.remove_prefix() # Leave if if there is an error
raise
finally:
# Once the install is done, destroy the stage where we built it,
# unless the user wants it kept around.
if not self.dirty:
self.stage.destroy() self.stage.destroy()
tty.msg("Successfully installed %s" % self.name)
print_pkg(self.prefix)
sys.exit(0)
except Exception, e:
self.remove_prefix()
raise
# Parent process just waits for the child to complete. If the
# child exited badly, assume it already printed an appropriate
# message. Just make the parent exit with an error code.
pid, returncode = os.waitpid(pid, 0)
if returncode != 0:
sys.exit(1)
def do_install_dependencies(self): def do_install_dependencies(self):
# Pass along paths of dependencies here # Pass along paths of dependencies here
@ -684,16 +711,16 @@ def module(self):
def install(self, spec, prefix): def install(self, spec, prefix):
"""Package implementations override this with their own build configuration.""" """Package implementations override this with their own build configuration."""
tty.die("Packages must provide an install method!") raise InstallError("Package %s provides no install method!" % self.name)
def do_uninstall(self): def do_uninstall(self):
if not self.installed: if not self.installed:
tty.die(self.name + " is not installed.") raise InstallError(self.name + " is not installed.")
if not self.ignore_dependencies: if not self.ignore_dependencies:
deps = self.installed_dependents deps = self.installed_dependents
if deps: tty.die( if deps: raise InstallError(
"Cannot uninstall %s. The following installed packages depend on it: %s" "Cannot uninstall %s. The following installed packages depend on it: %s"
% (self.name, deps)) % (self.name, deps))
@ -817,7 +844,32 @@ def print_pkg(message):
print message print message
class InvalidPackageDependencyError(spack.error.SpackError):
class FetchError(spack.error.SpackError):
"""Raised when something goes wrong during fetch."""
def __init__(self, message, long_msg=None):
super(FetchError, self).__init__(message, long_msg)
class ChecksumError(FetchError):
"""Raised when archive fails to checksum."""
def __init__(self, message, long_msg):
super(ChecksumError, self).__init__(message, long_msg)
class InstallError(spack.error.SpackError):
"""Raised when something goes wrong during install or uninstall."""
def __init__(self, message, long_msg=None):
super(InstallError, self).__init__(message, long_msg)
class PackageError(spack.error.SpackError):
"""Raised when something is wrong with a package definition."""
def __init__(self, message, long_msg=None):
super(PackageError, self).__init__(message, long_msg)
class InvalidPackageDependencyError(PackageError):
"""Raised when package specification is inconsistent with requirements of """Raised when package specification is inconsistent with requirements of
its dependencies.""" its dependencies."""
def __init__(self, message): def __init__(self, message):

View File

@ -93,6 +93,6 @@ def test_install_and_uninstall(self):
try: try:
pkg.do_install() pkg.do_install()
pkg.do_uninstall() pkg.do_uninstall()
except: except Exception, e:
if pkg: pkg.remove_prefix() if pkg: pkg.remove_prefix()
raise raise