diff --git a/easybuild/tools/config.py b/easybuild/tools/config.py index 4bcebbe158..aee7d4dcb4 100644 --- a/easybuild/tools/config.py +++ b/easybuild/tools/config.py @@ -231,6 +231,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX): 'amdgcn_capabilities', 'backup_modules', 'banned_linked_shared_libs', + 'breakpoints', 'checksum_priority', 'container_config', 'container_image_format', diff --git a/easybuild/tools/hooks.py b/easybuild/tools/hooks.py index 4451439856..d6e3ba6247 100644 --- a/easybuild/tools/hooks.py +++ b/easybuild/tools/hooks.py @@ -220,6 +220,34 @@ def find_hook(label, hooks, pre_step_hook=False, post_step_hook=False): return res +def _bash_breakpoint(*args, **kwargs): + """Simple breakpoint hook that opens a bash shell.""" + old_ps1 = os.environ.get('PS1', '') + old_promptcmd = os.environ.get('PROMPT_COMMAND', '') + os.environ['PROMPT_COMMAND'] = '' + os.environ['PS1'] = 'easybuild-breakpoint> ' + os.system('bash --norc --noprofile') + os.environ['PS1'] = old_ps1 + os.environ['PROMPT_COMMAND'] = old_promptcmd + + +def _python_breakpoint(*args, **kwargs): + """Simple breakpoint hook that opens a Python shell.""" + print('Python breakpoint reached, entering pdb shell...') + print('You can inspect/modify the state of the program from here and use `continue` to proceed.') + print('Arguments passed to this hook (will contain EasyBlock object to inspect/modify if available):') + print(' args = %s' % (args,)) + print(' kwargs = %s' % (kwargs,)) + import pdb + pdb.set_trace() + + +breakpoint_types = { + 'bash': _bash_breakpoint, + 'python': _python_breakpoint, +} + + def run_hook(label, hooks, pre_step_hook=False, post_step_hook=False, args=None, kwargs=None, msg=None): """ Run hook with specified label and return result of calling the hook or None. @@ -231,14 +259,28 @@ def run_hook(label, hooks, pre_step_hook=False, post_step_hook=False, args=None, :param args: arguments to pass to hook function :param msg: custom message that is printed when hook is called """ + args = args or [] + kwargs = kwargs or {} + + breakpoints = build_option('breakpoints') + bk_hooks = {} + if breakpoints: + for bk in breakpoints: + bk_type, bk_label = (['bash'] + bk.split(':', 1))[-2:] + if bk_type not in breakpoint_types: + raise EasyBuildError("Unknown breakpoint type '%s' specified for breakpoint '%s'", bk_type, bk) + hook_func = breakpoint_types.get(bk_type) + if not bk_label.endswith(HOOK_SUFF): + bk_label += HOOK_SUFF + bk_hooks[bk_label] = hook_func + + bp_hook = find_hook(label, bk_hooks, pre_step_hook=pre_step_hook, post_step_hook=post_step_hook) + if bp_hook: + bp_hook(*args, **kwargs) + hook = find_hook(label, hooks, pre_step_hook=pre_step_hook, post_step_hook=post_step_hook) res = None if hook: - if args is None: - args = [] - if kwargs is None: - kwargs = {} - if pre_step_hook: label = 'pre-' + label elif post_step_hook: diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py index c07c878f56..bb717b3373 100644 --- a/easybuild/tools/options.py +++ b/easybuild/tools/options.py @@ -627,6 +627,10 @@ def config_options(self): None, "store_true", False,), 'avail-repositories': ("Show all repository types (incl. non-usable)", None, "store_true", False,), + 'breakpoints': ( + "Drop into an interactive shell on the specified steps (use same names as for hooks comma separated)", + 'strlist', 'store', None + ), 'buildpath': ("Temporary build path", None, 'store', mk_full_default_path('buildpath')), 'containerpath': ("Location where container recipe & image will be stored", None, 'store', mk_full_default_path('containerpath')),