Add --show-log-on-error option to spack install

- converted `log_path` and `env_path` to properties of PackageBase.

- InstallErrors in build_environment are now annotated with the package
  that caused them, in the 'pkg' attribute.

- Add `--show-log-on-error` option to `spack install` that catches
  InstallErrors and prints the log to stderr if it exists.

Note that adding a reference to the Pakcage allows a lot of stuff
currently handled by do_install() and build_environment to be handled
externally.
This commit is contained in:
Todd Gamblin 2017-09-17 15:07:44 -07:00
parent 742cd7f127
commit c7a789e2d6
4 changed files with 59 additions and 14 deletions

View File

@ -598,6 +598,10 @@ def child_process(child_pipe, input_stream):
target=child_process, args=(child_pipe, input_stream)) target=child_process, args=(child_pipe, input_stream))
p.start() p.start()
except InstallError as e:
e.pkg = pkg
raise
finally: finally:
# Close the input stream in the parent process # Close the input stream in the parent process
if input_stream is not None: if input_stream is not None:
@ -606,6 +610,10 @@ def child_process(child_pipe, input_stream):
child_result = parent_pipe.recv() child_result = parent_pipe.recv()
p.join() p.join()
# let the caller know which package went wrong.
if isinstance(child_result, InstallError):
child_result.pkg = pkg
# If the child process raised an error, print its output here rather # If the child process raised an error, print its output here rather
# than waiting until the call to SpackError.die() in main(). This # than waiting until the call to SpackError.die() in main(). This
# allows exception handling output to be logged from within Spack. # allows exception handling output to be logged from within Spack.
@ -676,10 +684,15 @@ def make_stack(tb, stack=None):
class InstallError(spack.error.SpackError): class InstallError(spack.error.SpackError):
"""Raised by packages when a package fails to install""" """Raised by packages when a package fails to install.
Any subclass of InstallError will be annotated by Spack wtih a
``pkg`` attribute on failure, which the caller can use to get the
package for which the exception was raised.
"""
class ChildError(spack.error.SpackError): class ChildError(InstallError):
"""Special exception class for wrapping exceptions from child processes """Special exception class for wrapping exceptions from child processes
in Spack's build environment. in Spack's build environment.

View File

@ -27,6 +27,8 @@
import functools import functools
import os import os
import platform import platform
import shutil
import sys
import time import time
import xml.dom.minidom import xml.dom.minidom
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -68,6 +70,9 @@ def setup_parser(subparser):
subparser.add_argument( subparser.add_argument(
'--restage', action='store_true', '--restage', action='store_true',
help="if a partial install is detected, delete prior state") help="if a partial install is detected, delete prior state")
subparser.add_argument(
'--show-log-on-error', action='store_true',
help="print full build log to stderr if build fails")
subparser.add_argument( subparser.add_argument(
'--source', action='store_true', dest='install_source', '--source', action='store_true', dest='install_source',
help="install source files in prefix") help="install source files in prefix")
@ -367,13 +372,26 @@ def install(parser, args, **kwargs):
for s in spec.dependencies(): for s in spec.dependencies():
p = spack.repo.get(s) p = spack.repo.get(s)
p.do_install(**kwargs) p.do_install(**kwargs)
else: else:
package = spack.repo.get(spec) package = spack.repo.get(spec)
kwargs['explicit'] = True kwargs['explicit'] = True
package.do_install(**kwargs) package.do_install(**kwargs)
except InstallError as e:
if args.show_log_on_error:
e.print_context()
if not os.path.exists(e.pkg.build_log_path):
tty.error("'spack install' created no log.")
else:
sys.stderr.write('Full build log:\n')
with open(e.pkg.build_log_path) as log:
shutil.copyfileobj(log, sys.stderr)
raise
finally: finally:
PackageBase.do_install = saved_do_install PackageBase.do_install = saved_do_install
# Dump log file if asked to # Dump test output if asked to
if args.log_format is not None: if args.log_format is not None:
test_suite.dump(log_filename) test_suite.dump(log_filename)

View File

@ -785,6 +785,14 @@ def stage(self, stage):
"""Allow a stage object to be set to override the default.""" """Allow a stage object to be set to override the default."""
self._stage = stage self._stage = stage
@property
def env_path(self):
return os.path.join(self.stage.source_path, 'spack-build.env')
@property
def log_path(self):
return os.path.join(self.stage.source_path, 'spack-build.out')
def _make_fetcher(self): def _make_fetcher(self):
# Construct a composite fetcher that always contains at least # Construct a composite fetcher that always contains at least
# one element (the root package). In case there are resources # one element (the root package). In case there are resources
@ -1331,20 +1339,11 @@ def build_process():
self.stage.chdir_to_source() self.stage.chdir_to_source()
# Save the build environment in a file before building. # Save the build environment in a file before building.
env_path = join_path(os.getcwd(), 'spack-build.env') dump_environment(self.env_path)
# Redirect I/O to a build log (and optionally to
# the terminal)
log_path = join_path(os.getcwd(), 'spack-build.out')
# FIXME : refactor this assignment
self.log_path = log_path
self.env_path = env_path
dump_environment(env_path)
# Spawn a daemon that reads from a pipe and redirects # Spawn a daemon that reads from a pipe and redirects
# everything to log_path # everything to log_path
with log_output(log_path, echo, True) as logger: with log_output(self.log_path, echo, True) as logger:
for phase_name, phase_attr in zip( for phase_name, phase_attr in zip(
self.phases, self._InstallPhase_phases): self.phases, self._InstallPhase_phases):

View File

@ -142,3 +142,18 @@ def test_install_with_source(
spec.prefix.share, 'trivial-install-test-package', 'src') spec.prefix.share, 'trivial-install-test-package', 'src')
assert filecmp.cmp(os.path.join(mock_archive.path, 'configure'), assert filecmp.cmp(os.path.join(mock_archive.path, 'configure'),
os.path.join(src, 'configure')) os.path.join(src, 'configure'))
def test_show_log_on_error(builtin_mock, mock_archive, mock_fetch,
config, install_mockery, capfd):
"""Make sure --show-log-on-error works."""
with capfd.disabled():
out = install('--show-log-on-error', 'build-error',
fail_on_error=False)
assert isinstance(install.error, spack.build_environment.ChildError)
assert install.error.pkg.name == 'build-error'
assert 'Full build log:' in out
errors = [line for line in out.split('\n')
if 'configure: error: cannot run C compiled programs' in line]
assert len(errors) == 2