From ce9f632beb095268799da48f769734c20e637fbe Mon Sep 17 00:00:00 2001 From: crivella Date: Thu, 30 Oct 2025 16:28:38 +0100 Subject: [PATCH 1/6] Add breakpoints to EB --- easybuild/tools/config.py | 1 + easybuild/tools/hooks.py | 20 ++++++++++++++++++++ easybuild/tools/options.py | 4 ++++ 3 files changed, 25 insertions(+) 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..d8d9875169 100644 --- a/easybuild/tools/hooks.py +++ b/easybuild/tools/hooks.py @@ -219,6 +219,15 @@ def find_hook(label, hooks, pre_step_hook=False, post_step_hook=False): return res +def _breakpoint(): + """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'] = '\e[01;32measybuild-breakpoint> \e[00m' + os.system('bash --norc --noprofile') + os.environ['PS1'] = old_ps1 + os.environ['PROMPT_COMMAND'] = old_promptcmd def run_hook(label, hooks, pre_step_hook=False, post_step_hook=False, args=None, kwargs=None, msg=None): """ @@ -231,6 +240,17 @@ 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 """ + breakpoints = build_option('breakpoints') + bk_hooks = {} + if breakpoints: + for bk in breakpoints.split(','): + if not bk.endswith(HOOK_SUFF): + bk += HOOK_SUFF + bk_hooks[bk] = lambda *a, **k: _breakpoint() + bp_hook = find_hook(label, bk_hooks, pre_step_hook=pre_step_hook, post_step_hook=post_step_hook) + if bp_hook: + bp_hook() + hook = find_hook(label, hooks, pre_step_hook=pre_step_hook, post_step_hook=post_step_hook) res = None if 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')), From db22f7bb486e003bb3d15eef9e6a5037f7af0e86 Mon Sep 17 00:00:00 2001 From: crivella Date: Thu, 30 Oct 2025 16:53:30 +0100 Subject: [PATCH 2/6] lint + remove ANSI escape codes for color --- easybuild/tools/hooks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/easybuild/tools/hooks.py b/easybuild/tools/hooks.py index d8d9875169..63c0c31648 100644 --- a/easybuild/tools/hooks.py +++ b/easybuild/tools/hooks.py @@ -219,16 +219,18 @@ def find_hook(label, hooks, pre_step_hook=False, post_step_hook=False): return res + def _breakpoint(): """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'] = '\e[01;32measybuild-breakpoint> \e[00m' + os.environ['PS1'] = 'easybuild-breakpoint> ' os.system('bash --norc --noprofile') os.environ['PS1'] = old_ps1 os.environ['PROMPT_COMMAND'] = old_promptcmd + 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. From e645c2f977d68999cb1ac9d7f0fca39b093e05e3 Mon Sep 17 00:00:00 2001 From: crivella Date: Thu, 30 Oct 2025 17:07:59 +0100 Subject: [PATCH 3/6] Do not need the `split` whit using `strlist` for the option type --- easybuild/tools/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/tools/hooks.py b/easybuild/tools/hooks.py index 63c0c31648..9414d785ca 100644 --- a/easybuild/tools/hooks.py +++ b/easybuild/tools/hooks.py @@ -245,7 +245,7 @@ def run_hook(label, hooks, pre_step_hook=False, post_step_hook=False, args=None, breakpoints = build_option('breakpoints') bk_hooks = {} if breakpoints: - for bk in breakpoints.split(','): + for bk in breakpoints: if not bk.endswith(HOOK_SUFF): bk += HOOK_SUFF bk_hooks[bk] = lambda *a, **k: _breakpoint() From 820a46466acb81ff64983b0d6fd2bbd91c12c2eb Mon Sep 17 00:00:00 2001 From: crivella Date: Thu, 11 Dec 2025 17:07:32 +0100 Subject: [PATCH 4/6] Add also possibility to jump into a python shell --- easybuild/tools/hooks.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/easybuild/tools/hooks.py b/easybuild/tools/hooks.py index 9414d785ca..4690d5b38c 100644 --- a/easybuild/tools/hooks.py +++ b/easybuild/tools/hooks.py @@ -220,7 +220,7 @@ def find_hook(label, hooks, pre_step_hook=False, post_step_hook=False): return res -def _breakpoint(): +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', '') @@ -230,6 +230,20 @@ def _breakpoint(): 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): """ @@ -242,25 +256,30 @@ 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: - if not bk.endswith(HOOK_SUFF): - bk += HOOK_SUFF - bk_hooks[bk] = lambda *a, **k: _breakpoint() + if ':' in bk: + bk_type, bk_label = bk.split(':', 1) + else: + bk_type = 'bash' + bk_label = 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() + 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: From d80b9de8ad746e32e099bac5f7a67c4179a6cf48 Mon Sep 17 00:00:00 2001 From: crivella Date: Thu, 11 Dec 2025 17:08:19 +0100 Subject: [PATCH 5/6] lint --- easybuild/tools/hooks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/easybuild/tools/hooks.py b/easybuild/tools/hooks.py index 4690d5b38c..5de8e43513 100644 --- a/easybuild/tools/hooks.py +++ b/easybuild/tools/hooks.py @@ -230,6 +230,7 @@ def _bash_breakpoint(*args, **kwargs): 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...') @@ -240,11 +241,13 @@ def _python_breakpoint(*args, **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. From e6c90762733c1f2b823076be813e5c7684e4dd7b Mon Sep 17 00:00:00 2001 From: crivella Date: Thu, 11 Dec 2025 17:25:30 +0100 Subject: [PATCH 6/6] Check for non-existing breakpoint type --- easybuild/tools/hooks.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/easybuild/tools/hooks.py b/easybuild/tools/hooks.py index 5de8e43513..d6e3ba6247 100644 --- a/easybuild/tools/hooks.py +++ b/easybuild/tools/hooks.py @@ -266,11 +266,9 @@ def run_hook(label, hooks, pre_step_hook=False, post_step_hook=False, args=None, bk_hooks = {} if breakpoints: for bk in breakpoints: - if ':' in bk: - bk_type, bk_label = bk.split(':', 1) - else: - bk_type = 'bash' - bk_label = bk + 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