refactor: convert build_process to use BuildProcessInstaller (#25167)

`build_process` has been around a long time but it's become a very large,
unwieldy method. It's hard to work with because it has a lot of local
variables that need to persist across all of the code.

- [x] To address this, convert it its own `BuildInfoProcess` class.
- [x] Start breaking the method apart by factoring out the main
      installation logic into its own function.
This commit is contained in:
Todd Gamblin 2021-08-03 01:24:24 -07:00 committed by GitHub
parent 0a0338ddfa
commit cf8d1b0387
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -144,14 +144,8 @@ def _handle_external_and_upstream(pkg, explicit):
def _do_fake_install(pkg): def _do_fake_install(pkg):
"""Make a fake install directory with fake executables, headers, and libraries.
""" """
Make a fake install directory containing fake executables, headers,
and libraries.
Args:
pkg (spack.package.PackageBase): the package whose installation is to be faked
"""
command = pkg.name command = pkg.name
header = pkg.name header = pkg.name
library = pkg.name library = pkg.name
@ -1170,8 +1164,7 @@ def _install_task(self, task):
# stop early from clients, and is not an error at this point # stop early from clients, and is not an error at this point
pid = '{0}: '.format(pkg.pid) if tty.show_pid() else '' pid = '{0}: '.format(pkg.pid) if tty.show_pid() else ''
tty.debug('{0}{1}'.format(pid, str(e))) tty.debug('{0}{1}'.format(pid, str(e)))
tty.debug('Package stage directory: {0}' tty.debug('Package stage directory: {0}' .format(pkg.stage.source_path))
.format(pkg.stage.source_path))
def _next_is_pri0(self): def _next_is_pri0(self):
""" """
@ -1678,66 +1671,126 @@ def install(self):
'reported errors for failing package(s).') 'reported errors for failing package(s).')
def build_process(pkg, kwargs): class BuildProcessInstaller(object):
"""Perform the installation/build of the package. """This class implements the part installation that happens in the child process."""
This runs in a separate child process, and has its own process and def __init__(self, pkg, install_args):
python module space set up by build_environment.start_build_process(). """Create a new BuildProcessInstaller.
It is assumed that the lifecycle of this object is the same as the child
process in the build.
Arguments:
pkg (spack.package.PackageBase) the package being installed.
install_args (dict) arguments to do_install() from parent process.
This function's return value is returned to the parent process.
""" """
fake = kwargs.get('fake', False) self.pkg = pkg
install_source = kwargs.get('install_source', False)
keep_stage = kwargs.get('keep_stage', False)
skip_patch = kwargs.get('skip_patch', False)
unmodified_env = kwargs.get('unmodified_env', {})
verbose = kwargs.get('verbose', False)
timer = Timer() # whether to do a fake install
self.fake = install_args.get('fake', False)
# whether to install source code with the packag
self.install_source = install_args.get('install_source', False)
# whether to keep the build stage after installation
self.keep_stage = install_args.get('keep_stage', False)
# whether to skip the patch phase
self.skip_patch = install_args.get('skip_patch', False)
# whether to enable echoing of build output initially or not
self.verbose = install_args.get('verbose', False)
# env before starting installation
self.unmodified_env = install_args.get('unmodified_env', {})
# timer for build phases
self.timer = Timer()
# If we are using a padded path, filter the output to compress padded paths # If we are using a padded path, filter the output to compress padded paths
# The real log still has full-length paths. # The real log still has full-length paths.
filter_padding = spack.config.get("config:install_tree:padded_length", None) filter_padding = spack.config.get("config:install_tree:padded_length", None)
filter_fn = spack.util.path.padding_filter if filter_padding else None self.filter_fn = spack.util.path.padding_filter if filter_padding else None
if not fake:
if not skip_patch:
pkg.do_patch()
else:
pkg.do_stage()
# info/debug information
pid = '{0}: '.format(pkg.pid) if tty.show_pid() else '' pid = '{0}: '.format(pkg.pid) if tty.show_pid() else ''
pre = '{0}{1}:'.format(pid, pkg.name) self.pre = '{0}{1}:'.format(pid, pkg.name)
pkg_id = package_id(pkg) self.pkg_id = package_id(pkg)
tty.debug('{0} Building {1} [{2}]' def run(self):
.format(pre, pkg_id, pkg.build_system_class)) """Main entry point from ``build_process`` to kick off install in child."""
if not self.fake:
if not self.skip_patch:
self.pkg.do_patch()
else:
self.pkg.do_stage()
tty.debug(
'{0} Building {1} [{2}]' .format(
self.pre,
self.pkg_id,
self.pkg.build_system_class
)
)
# get verbosity from do_install() parameter or saved value # get verbosity from do_install() parameter or saved value
echo = verbose self.echo = self.verbose
if spack.package.PackageBase._verbose is not None: if spack.package.PackageBase._verbose is not None:
echo = spack.package.PackageBase._verbose self.echo = spack.package.PackageBase._verbose
pkg.stage.keep = keep_stage self.pkg.stage.keep = self.keep_stage
with pkg.stage: with self.pkg.stage:
# Run the pre-install hook in the child process after # Run the pre-install hook in the child process after
# the directory is created. # the directory is created.
spack.hooks.pre_install(pkg.spec) spack.hooks.pre_install(self.pkg.spec)
if fake: if self.fake:
_do_fake_install(pkg) _do_fake_install(self.pkg)
else: else:
source_path = pkg.stage.source_path if self.install_source:
if install_source and os.path.isdir(source_path): self._install_source()
src_target = os.path.join(pkg.spec.prefix, 'share',
pkg.name, 'src') self._real_install()
tty.debug('{0} Copying source to {1}'
.format(pre, src_target)) # Stop the timer and save results
self.timer.stop()
with open(self.pkg.times_log_path, 'w') as timelog:
self.timer.write_json(timelog)
# Run post install hooks before build stage is removed.
spack.hooks.post_install(self.pkg.spec)
build_time = self.timer.total - self.pkg._fetch_time
tty.msg('{0} Successfully installed {1}'.format(self.pre, self.pkg_id),
'Fetch: {0}. Build: {1}. Total: {2}.'
.format(_hms(self.pkg._fetch_time), _hms(build_time),
_hms(self.timer.total)))
_print_installed_pkg(self.pkg.prefix)
# Send final status that install is successful
spack.hooks.on_install_success(self.pkg.spec)
# preserve verbosity across runs
return self.echo
def _install_source(self):
"""Install source code from stage into share/pkg/src if necessary."""
pkg = self.pkg
if not os.path.isdir(pkg.stage.source_path):
return
src_target = os.path.join(pkg.spec.prefix, 'share', pkg.name, 'src')
tty.debug('{0} Copying source to {1}' .format(self.pre, src_target))
fs.install_tree(pkg.stage.source_path, src_target) fs.install_tree(pkg.stage.source_path, src_target)
def _real_install(self):
pkg = self.pkg
# Do the real install in the source directory. # Do the real install in the source directory.
with fs.working_dir(pkg.stage.source_path): with fs.working_dir(pkg.stage.source_path):
# Save the build environment in a file before building. # Save the build environment in a file before building.
dump_environment(pkg.env_path) dump_environment(pkg.env_path)
@ -1772,21 +1825,29 @@ def build_process(pkg, kwargs):
try: try:
# DEBUGGING TIP - to debug this section, insert an IPython # DEBUGGING TIP - to debug this section, insert an IPython
# embed here, and run the sections below without log capture # embed here, and run the sections below without log capture
with log_output( log_contextmanager = log_output(
log_file, echo, True, env=unmodified_env, log_file,
filter_fn=filter_fn self.echo,
) as logger: True,
env=self.unmodified_env,
filter_fn=self.filter_fn
)
with log_contextmanager as logger:
with logger.force_echo(): with logger.force_echo():
inner_debug_level = tty.debug_level() inner_debug_level = tty.debug_level()
tty.set_debug(debug_level) tty.set_debug(debug_level)
tty.msg("{0} Executing phase: '{1}'" tty.msg(
.format(pre, phase_name)) "{0} Executing phase: '{1}'" .format(
self.pre,
phase_name
)
)
tty.set_debug(inner_debug_level) tty.set_debug(inner_debug_level)
# Redirect stdout and stderr to daemon pipe # Redirect stdout and stderr to daemon pipe
phase = getattr(pkg, phase_attr) phase = getattr(pkg, phase_attr)
timer.phase(phase_name) self.timer.phase(phase_name)
# Catch any errors to report to logging # Catch any errors to report to logging
phase(pkg.spec, pkg.prefix) phase(pkg.spec, pkg.prefix)
@ -1798,32 +1859,31 @@ def build_process(pkg, kwargs):
raise raise
# We assume loggers share echo True/False # We assume loggers share echo True/False
echo = logger.echo self.echo = logger.echo
# After log, we can get all output/error files from the package stage # After log, we can get all output/error files from the package stage
combine_phase_logs(pkg.phase_log_files, pkg.log_path) combine_phase_logs(pkg.phase_log_files, pkg.log_path)
log(pkg) log(pkg)
# Stop the timer and save results
timer.stop()
with open(pkg.times_log_path, 'w') as timelog:
timer.write_json(timelog)
# Run post install hooks before build stage is removed. def build_process(pkg, install_args):
spack.hooks.post_install(pkg.spec) """Perform the installation/build of the package.
build_time = timer.total - pkg._fetch_time This runs in a separate child process, and has its own process and
tty.msg('{0} Successfully installed {1}'.format(pre, pkg_id), python module space set up by build_environment.start_build_process().
'Fetch: {0}. Build: {1}. Total: {2}.'
.format(_hms(pkg._fetch_time), _hms(build_time),
_hms(timer.total)))
_print_installed_pkg(pkg.prefix)
# Send final status that install is successful This essentially wraps an instance of ``BuildProcessInstaller`` so that we can
spack.hooks.on_install_success(pkg.spec) more easily create one in a subprocess.
# preserve verbosity across runs This function's return value is returned to the parent process.
return echo
Arguments:
pkg (spack.package.PackageBase): the package being installed.
install_args (dict): arguments to do_install() from parent process.
"""
installer = BuildProcessInstaller(pkg, install_args)
return installer.run()
class BuildTask(object): class BuildTask(object):