4545import shutil
4646import string
4747import subprocess
48- import sys
4948import tempfile
5049import time
5150from collections import namedtuple
6867from easybuild .tools .build_log import dry_run_msg , print_msg , time_str_since
6968from easybuild .tools .config import build_option
7069from easybuild .tools .hooks import RUN_SHELL_CMD , load_hooks , run_hook
70+ from easybuild .tools .output import COLOR_RED , COLOR_YELLOW , colorize , print_error
7171from easybuild .tools .utilities import trace_msg
7272
7373
8686)
8787
8888RunShellCmdResult = namedtuple ('RunShellCmdResult' , ('cmd' , 'exit_code' , 'output' , 'stderr' , 'work_dir' ,
89- 'out_file' , 'err_file' , 'thread_id' , 'task_id' ))
89+ 'out_file' , 'err_file' , 'cmd_sh' , 'thread_id' , 'task_id' ))
90+ RunShellCmdResult .__doc__ = """A namedtuple that represents the result of a call to run_shell_cmd,
91+ with the following fields:
92+ - cmd: the command that was executed;
93+ - exit_code: the exit code of the command (zero if it was successful, non-zero if not);
94+ - output: output of the command (stdout+stderr combined, only stdout if stderr was caught separately);
95+ - stderr: stderr output produced by the command, if caught separately (None otherwise);
96+ - work_dir: the working directory of the command;
97+ - out_file: path to file with output of command (stdout+stderr combined, only stdout if stderr was caught separately);
98+ - err_file: path to file with stderr output of command, if caught separately (None otherwise);
99+ - cmd_sh: path to script to set up interactive shell with environment in which command was executed;
100+ - thread_id: thread ID of command that was executed (None unless asynchronous mode was enabled for running command);
101+ - task_id: task ID of command, if it was specified (None otherwise);
102+ """
90103
91104
92105class RunShellCmdError (BaseException ):
@@ -101,6 +114,7 @@ def __init__(self, cmd_result, caller_info, *args, **kwargs):
101114 self .out_file = cmd_result .out_file
102115 self .stderr = cmd_result .stderr
103116 self .err_file = cmd_result .err_file
117+ self .cmd_sh = cmd_result .cmd_sh
104118
105119 self .caller_info = caller_info
106120
@@ -112,33 +126,36 @@ def print(self):
112126 Report failed shell command for this RunShellCmdError instance
113127 """
114128
115- def pad_4_spaces (msg ):
116- return ' ' * 4 + msg
129+ def pad_4_spaces (msg , color = None ):
130+ padded_msg = ' ' * 4 + msg
131+ if color :
132+ return colorize (padded_msg , color )
133+ else :
134+ return padded_msg
135+
136+ caller_file_name , caller_line_nr , caller_function_name = self .caller_info
137+ called_from_info = f"'{ caller_function_name } ' function in { caller_file_name } (line { caller_line_nr } )"
117138
118139 error_info = [
119- '' ,
120- "ERROR: Shell command failed!" ,
140+ colorize ("ERROR: Shell command failed!" , COLOR_RED ),
121141 pad_4_spaces (f"full command -> { self .cmd } " ),
122142 pad_4_spaces (f"exit code -> { self .exit_code } " ),
143+ pad_4_spaces (f"called from -> { called_from_info } " ),
123144 pad_4_spaces (f"working directory -> { self .work_dir } " ),
124145 ]
125146
126147 if self .out_file is not None :
127148 # if there's no separate file for error/warnings, then out_file includes both stdout + stderr
128149 out_info_msg = "output (stdout + stderr)" if self .err_file is None else "output (stdout) "
129- error_info .append (pad_4_spaces (f"{ out_info_msg } -> { self .out_file } " ))
150+ error_info .append (pad_4_spaces (f"{ out_info_msg } -> { self .out_file } " , color = COLOR_YELLOW ))
130151
131152 if self .err_file is not None :
132- error_info .append (pad_4_spaces (f"error/warnings (stderr) -> { self .err_file } " ))
153+ error_info .append (pad_4_spaces (f"error/warnings (stderr) -> { self .err_file } " , color = COLOR_YELLOW ))
133154
134- caller_file_name , caller_line_nr , caller_function_name = self .caller_info
135- called_from_info = f"'{ caller_function_name } ' function in { caller_file_name } (line { caller_line_nr } )"
136- error_info .extend ([
137- pad_4_spaces (f"called from -> { called_from_info } " ),
138- '' ,
139- ])
155+ if self .cmd_sh is not None :
156+ error_info .append (pad_4_spaces (f"interactive shell script -> { self .cmd_sh } " , color = COLOR_YELLOW ))
140157
141- sys . stderr . write ('\n ' .join (error_info ) + ' \n ' )
158+ print_error ('\n ' .join (error_info ), rich_highlight = False )
142159
143160
144161def raise_run_shell_cmd_error (cmd_res ):
@@ -254,6 +271,8 @@ def create_cmd_scripts(cmd_str, work_dir, env, tmpdir, out_file, err_file):
254271 ]))
255272 os .chmod (cmd_fp , 0o775 )
256273
274+ return cmd_fp
275+
257276
258277def _answer_question (stdout , proc , qa_patterns , qa_wait_patterns ):
259278 """
@@ -430,9 +449,9 @@ def to_cmd_str(cmd):
430449 else :
431450 cmd_err_fp = None
432451
433- create_cmd_scripts (cmd_str , work_dir , env , tmpdir , cmd_out_fp , cmd_err_fp )
452+ cmd_sh = create_cmd_scripts (cmd_str , work_dir , env , tmpdir , cmd_out_fp , cmd_err_fp )
434453 else :
435- tmpdir , cmd_out_fp , cmd_err_fp = None , None , None
454+ tmpdir , cmd_out_fp , cmd_err_fp , cmd_sh = None , None , None , None
436455
437456 interactive_msg = 'interactive ' if interactive else ''
438457
@@ -445,7 +464,8 @@ def to_cmd_str(cmd):
445464 dry_run_msg (msg , silent = silent )
446465
447466 return RunShellCmdResult (cmd = cmd_str , exit_code = 0 , output = '' , stderr = None , work_dir = work_dir ,
448- out_file = cmd_out_fp , err_file = cmd_err_fp , thread_id = thread_id , task_id = task_id )
467+ out_file = cmd_out_fp , err_file = cmd_err_fp , cmd_sh = cmd_sh ,
468+ thread_id = thread_id , task_id = task_id )
449469
450470 start_time = datetime .now ()
451471 if not hidden :
@@ -574,8 +594,9 @@ def to_cmd_str(cmd):
574594 except IOError as err :
575595 raise EasyBuildError (f"Failed to dump command output to temporary file: { err } " )
576596
577- res = RunShellCmdResult (cmd = cmd_str , exit_code = proc .returncode , output = output , stderr = stderr , work_dir = work_dir ,
578- out_file = cmd_out_fp , err_file = cmd_err_fp , thread_id = thread_id , task_id = task_id )
597+ res = RunShellCmdResult (cmd = cmd_str , exit_code = proc .returncode , output = output , stderr = stderr ,
598+ work_dir = work_dir , out_file = cmd_out_fp , err_file = cmd_err_fp , cmd_sh = cmd_sh ,
599+ thread_id = thread_id , task_id = task_id )
579600
580601 # always log command output
581602 cmd_name = cmd_str .split (' ' )[0 ]
0 commit comments