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))
p.start()
except InstallError as e:
e.pkg = pkg
raise
finally:
# Close the input stream in the parent process
if input_stream is not None:
@ -606,6 +610,10 @@ def child_process(child_pipe, input_stream):
child_result = parent_pipe.recv()
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
# than waiting until the call to SpackError.die() in main(). This
# 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):
"""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
in Spack's build environment.

View File

@ -27,6 +27,8 @@
import functools
import os
import platform
import shutil
import sys
import time
import xml.dom.minidom
import xml.etree.ElementTree as ET
@ -68,6 +70,9 @@ def setup_parser(subparser):
subparser.add_argument(
'--restage', action='store_true',
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(
'--source', action='store_true', dest='install_source',
help="install source files in prefix")
@ -367,13 +372,26 @@ def install(parser, args, **kwargs):
for s in spec.dependencies():
p = spack.repo.get(s)
p.do_install(**kwargs)
else:
package = spack.repo.get(spec)
kwargs['explicit'] = True
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:
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:
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."""
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):
# Construct a composite fetcher that always contains at least
# one element (the root package). In case there are resources
@ -1331,20 +1339,11 @@ def build_process():
self.stage.chdir_to_source()
# Save the build environment in a file before building.
env_path = join_path(os.getcwd(), 'spack-build.env')
# 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)
dump_environment(self.env_path)
# Spawn a daemon that reads from a pipe and redirects
# 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(
self.phases, self._InstallPhase_phases):

View File

@ -142,3 +142,18 @@ def test_install_with_source(
spec.prefix.share, 'trivial-install-test-package', 'src')
assert filecmp.cmp(os.path.join(mock_archive.path, '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