Make install command reusable within single Spack run

- install and probably other commands were designed to run once, but now
  we can to test them from within Spack with SpackCommand

- cmd/install.py assumed that it could modify do_install in PackageBase
  and leave it that way; this makes the decorator temporary

- package.py didn't properly initialize its stage if the same package had
  been built successfully before (and the stage removed).
  - manage stage lifecycle better and remember when Package needs to
    re-create the stage
This commit is contained in:
Todd Gamblin 2017-08-22 14:35:17 -07:00
parent fa1faa61c4
commit f51b541ef8
3 changed files with 48 additions and 21 deletions

View File

@ -334,20 +334,28 @@ def install(parser, args, **kwargs):
tty.error('The `spack install` command requires a spec to install.') tty.error('The `spack install` command requires a spec to install.')
for spec in specs: for spec in specs:
saved_do_install = PackageBase.do_install
decorator = lambda fn: fn
# Check if we were asked to produce some log for dashboards # Check if we were asked to produce some log for dashboards
if args.log_format is not None: if args.log_format is not None:
# Compute the filename for logging # Compute the filename for logging
log_filename = args.log_file log_filename = args.log_file
if not log_filename: if not log_filename:
log_filename = default_log_file(spec) log_filename = default_log_file(spec)
# Create the test suite in which to log results # Create the test suite in which to log results
test_suite = TestSuite(spec) test_suite = TestSuite(spec)
# Decorate PackageBase.do_install to get installation status
PackageBase.do_install = junit_output( # Temporarily decorate PackageBase.do_install to monitor
spec, test_suite # recursive calls.
)(PackageBase.do_install) decorator = junit_output(spec, test_suite)
# Do the actual installation # Do the actual installation
try:
# decorate the install if necessary
PackageBase.do_install = decorator(PackageBase.do_install)
if args.things_to_install == 'dependencies': if args.things_to_install == 'dependencies':
# Install dependencies as-if they were installed # Install dependencies as-if they were installed
# for root (explicit=False in the DB) # for root (explicit=False in the DB)
@ -359,6 +367,8 @@ def install(parser, args, **kwargs):
package = spack.repo.get(spec) package = spack.repo.get(spec)
kwargs['explicit'] = True kwargs['explicit'] = True
package.do_install(**kwargs) package.do_install(**kwargs)
finally:
PackageBase.do_install = saved_do_install
# Dump log file if asked to # Dump log file if asked to
if args.log_format is not None: if args.log_format is not None:

View File

@ -760,10 +760,6 @@ def _make_stage(self):
# Append the item to the composite # Append the item to the composite
composite_stage.append(stage) composite_stage.append(stage)
# Create stage on first access. Needed because fetch, stage,
# patch, and install can be called independently of each
# other, so `with self.stage:` in do_install isn't sufficient.
composite_stage.create()
return composite_stage return composite_stage
@property @property
@ -772,6 +768,12 @@ def stage(self):
raise ValueError("Can only get a stage for a concrete package.") raise ValueError("Can only get a stage for a concrete package.")
if self._stage is None: if self._stage is None:
self._stage = self._make_stage() self._stage = self._make_stage()
# Create stage on first access. Needed because fetch, stage,
# patch, and install can be called independently of each
# other, so `with self.stage:` in do_install isn't sufficient.
self._stage.create()
return self._stage return self._stage
@stage.setter @stage.setter
@ -1390,6 +1392,11 @@ def build_process():
if not keep_prefix: if not keep_prefix:
self.remove_prefix() self.remove_prefix()
# The subprocess *may* have removed the build stage. Mark it
# not created so that the next time self.stage is invoked, we
# check the filesystem for it.
self.stage.created = False
def check_for_unfinished_installation( def check_for_unfinished_installation(
self, keep_prefix=False, restage=False): self, keep_prefix=False, restage=False):
"""Check for leftover files from partially-completed prior install to """Check for leftover files from partially-completed prior install to

View File

@ -236,6 +236,10 @@ def __init__(
self._lock = Stage.stage_locks[self.name] self._lock = Stage.stage_locks[self.name]
# When stages are reused, we need to know whether to re-create
# it. This marks whether it has been created/destroyed.
self.created = False
def __enter__(self): def __enter__(self):
""" """
Entering a stage context will create the stage directory Entering a stage context will create the stage directory
@ -521,6 +525,7 @@ def create(self):
mkdirp(self.path) mkdirp(self.path)
# Make sure we can actually do something with the stage we made. # Make sure we can actually do something with the stage we made.
ensure_access(self.path) ensure_access(self.path)
self.created = True
def destroy(self): def destroy(self):
"""Removes this stage directory.""" """Removes this stage directory."""
@ -532,6 +537,9 @@ def destroy(self):
except OSError: except OSError:
os.chdir(os.path.dirname(self.path)) os.chdir(os.path.dirname(self.path))
# mark as destroyed
self.created = False
class ResourceStage(Stage): class ResourceStage(Stage):
@ -573,8 +581,9 @@ def expand_archive(self):
shutil.move(source_path, destination_path) shutil.move(source_path, destination_path)
@pattern.composite(method_list=['fetch', 'create', 'check', 'expand_archive', @pattern.composite(method_list=[
'restage', 'destroy', 'cache_local']) 'fetch', 'create', 'created', 'check', 'expand_archive', 'restage',
'destroy', 'cache_local'])
class StageComposite: class StageComposite:
"""Composite for Stage type objects. The first item in this composite is """Composite for Stage type objects. The first item in this composite is
considered to be the root package, and operations that return a value are considered to be the root package, and operations that return a value are
@ -623,6 +632,7 @@ def __init__(self, path):
self.archive_file = None self.archive_file = None
self.path = path self.path = path
self.source_path = path self.source_path = path
self.created = True
def chdir(self): def chdir(self):
if os.path.isdir(self.path): if os.path.isdir(self.path):