7676]
7777
7878
79- RunShellCmdResult = namedtuple ('RunShellCmdResult' , ('cmd' , 'exit_code' , 'output' , 'stderr' , 'work_dir' ))
79+ RunShellCmdResult = namedtuple ('RunShellCmdResult' , ('cmd' , 'exit_code' , 'output' , 'stderr' , 'work_dir' ,
80+ 'out_file' , 'err_file' ))
8081
8182
8283class RunShellCmdError (BaseException ):
8384
84- def __init__ (self , cmd , exit_code , work_dir , output , stderr , caller_info , * args , ** kwargs ):
85+ def __init__ (self , cmd_result , caller_info , * args , ** kwargs ):
8586 """Constructor for RunShellCmdError."""
86- self .cmd = cmd
87- self .cmd_name = cmd .split (' ' )[0 ]
88- self .exit_code = exit_code
89- self .work_dir = work_dir
90- self .output = output
91- self .stderr = stderr
87+ self .cmd = cmd_result .cmd
88+ self .cmd_name = os .path .basename (self .cmd .split (' ' )[0 ])
89+ self .exit_code = cmd_result .exit_code
90+ self .work_dir = cmd_result .work_dir
91+ self .output = cmd_result .output
92+ self .out_file = cmd_result .out_file
93+ self .stderr = cmd_result .stderr
94+ self .err_file = cmd_result .err_file
95+
9296 self .caller_info = caller_info
9397
9498 msg = f"Shell command '{ self .cmd_name } ' failed!"
@@ -110,21 +114,13 @@ def pad_4_spaces(msg):
110114 pad_4_spaces (f"working directory -> { self .work_dir } " ),
111115 ]
112116
113- tmpdir = tempfile . mkdtemp ( prefix = 'shell-cmd-error-' )
114- output_fp = os . path . join ( tmpdir , f" { self . cmd_name } .out" )
115- with open ( output_fp , 'w' ) as fp :
116- fp . write ( self . output or '' )
117+ if self . out_file is not None :
118+ # if there's no separate file for error/warnings, then out_file includes both stdout + stderr
119+ out_info_msg = "output (stdout + stderr)" if self . err_file is None else "output (stdout) "
120+ error_info . append ( pad_4_spaces ( f" { out_info_msg } -> { self . out_file } " ) )
117121
118- if self .stderr is None :
119- error_info .append (pad_4_spaces (f"output (stdout + stderr) -> { output_fp } " ))
120- else :
121- stderr_fp = os .path .join (tmpdir , f"{ self .cmd_name } .err" )
122- with open (stderr_fp , 'w' ) as fp :
123- fp .write (self .stderr )
124- error_info .extend ([
125- pad_4_spaces (f"output (stdout) -> { output_fp } " ),
126- pad_4_spaces (f"error/warnings (stderr) -> { stderr_fp } " ),
127- ])
122+ if self .err_file is not None :
123+ error_info .append (pad_4_spaces (f"error/warnings (stderr) -> { self .err_file } " ))
128124
129125 caller_file_name , caller_line_nr , caller_function_name = self .caller_info
130126 called_from_info = f"'{ caller_function_name } ' function in { caller_file_name } (line { caller_line_nr } )"
@@ -136,9 +132,9 @@ def pad_4_spaces(msg):
136132 sys .stderr .write ('\n ' .join (error_info ) + '\n ' )
137133
138134
139- def raise_run_shell_cmd_error (cmd , exit_code , work_dir , output , stderr ):
135+ def raise_run_shell_cmd_error (cmd_res ):
140136 """
141- Raise RunShellCmdError for failing shell command, after collecting additional caller info
137+ Raise RunShellCmdError for failed shell command, after collecting additional caller info
142138 """
143139
144140 # figure out where failing command was run
@@ -150,7 +146,7 @@ def raise_run_shell_cmd_error(cmd, exit_code, work_dir, output, stderr):
150146 frameinfo = inspect .getouterframes (inspect .currentframe ())[3 ]
151147 caller_info = (frameinfo .filename , frameinfo .lineno , frameinfo .function )
152148
153- raise RunShellCmdError (cmd , exit_code , work_dir , output , stderr , caller_info )
149+ raise RunShellCmdError (cmd_res , caller_info )
154150
155151
156152def run_cmd_cache (func ):
@@ -185,7 +181,7 @@ def cache_aware_func(cmd, *args, **kwargs):
185181@run_shell_cmd_cache
186182def run_shell_cmd (cmd , fail_on_error = True , split_stderr = False , stdin = None , env = None ,
187183 hidden = False , in_dry_run = False , verbose_dry_run = False , work_dir = None , shell = True ,
188- output_file = False , stream_output = False , asynchronous = False , with_hooks = True ,
184+ output_file = True , stream_output = False , asynchronous = False , with_hooks = True ,
189185 qa_patterns = None , qa_wait_patterns = None ):
190186 """
191187 Run specified (interactive) shell command, and capture output + exit code.
@@ -234,16 +230,23 @@ def to_cmd_str(cmd):
234230 if work_dir is None :
235231 work_dir = os .getcwd ()
236232
237- # temporary output file for command output, if requested
238- if output_file or not hidden :
239- # collect output of running command in temporary log file, if desired
240- fd , cmd_out_fp = tempfile .mkstemp (suffix = '.log' , prefix = 'easybuild-run-' )
241- os .close (fd )
242- _log .info (f'run_cmd: Output of "{ cmd } " will be logged to { cmd_out_fp } ' )
243- else :
244- cmd_out_fp = None
245-
246233 cmd_str = to_cmd_str (cmd )
234+ cmd_name = os .path .basename (cmd_str .split (' ' )[0 ])
235+
236+ # temporary output file(s) for command output
237+ if output_file :
238+ toptmpdir = os .path .join (tempfile .gettempdir (), 'run-shell-cmd-output' )
239+ os .makedirs (toptmpdir , exist_ok = True )
240+ tmpdir = tempfile .mkdtemp (dir = toptmpdir , prefix = f'{ cmd_name } -' )
241+ cmd_out_fp = os .path .join (tmpdir , 'out.txt' )
242+ _log .info (f'run_cmd: Output of "{ cmd_str } " will be logged to { cmd_out_fp } ' )
243+ if split_stderr :
244+ cmd_err_fp = os .path .join (tmpdir , 'err.txt' )
245+ _log .info (f'run_cmd: Errors and warnings of "{ cmd_str } " will be logged to { cmd_err_fp } ' )
246+ else :
247+ cmd_err_fp = None
248+ else :
249+ cmd_out_fp , cmd_err_fp = None , None
247250
248251 # early exit in 'dry run' mode, after printing the command that would be run (unless 'hidden' is enabled)
249252 if not in_dry_run and build_option ('extended_dry_run' ):
@@ -253,11 +256,12 @@ def to_cmd_str(cmd):
253256 msg += f" (in { work_dir } )"
254257 dry_run_msg (msg , silent = silent )
255258
256- return RunShellCmdResult (cmd = cmd_str , exit_code = 0 , output = '' , stderr = None , work_dir = work_dir )
259+ return RunShellCmdResult (cmd = cmd_str , exit_code = 0 , output = '' , stderr = None , work_dir = work_dir ,
260+ out_file = cmd_out_fp , err_file = cmd_err_fp )
257261
258262 start_time = datetime .now ()
259263 if not hidden :
260- cmd_trace_msg (cmd_str , start_time , work_dir , stdin , cmd_out_fp )
264+ cmd_trace_msg (cmd_str , start_time , work_dir , stdin , cmd_out_fp , cmd_err_fp )
261265
262266 if stdin :
263267 # 'input' value fed to subprocess.run must be a byte sequence
@@ -286,7 +290,19 @@ def to_cmd_str(cmd):
286290 output = proc .stdout .decode ('utf-8' , 'ignore' )
287291 stderr = proc .stderr .decode ('utf-8' , 'ignore' ) if split_stderr else None
288292
289- res = RunShellCmdResult (cmd = cmd_str , exit_code = proc .returncode , output = output , stderr = stderr , work_dir = work_dir )
293+ # store command output to temporary file(s)
294+ if output_file :
295+ try :
296+ with open (cmd_out_fp , 'w' ) as fp :
297+ fp .write (output )
298+ if split_stderr :
299+ with open (cmd_err_fp , 'w' ) as fp :
300+ fp .write (stderr )
301+ except IOError as err :
302+ raise EasyBuildError (f"Failed to dump command output to temporary file: { err } " )
303+
304+ res = RunShellCmdResult (cmd = cmd_str , exit_code = proc .returncode , output = output , stderr = stderr , work_dir = work_dir ,
305+ out_file = cmd_out_fp , err_file = cmd_err_fp )
290306
291307 # always log command output
292308 cmd_name = cmd_str .split (' ' )[0 ]
@@ -301,7 +317,7 @@ def to_cmd_str(cmd):
301317 else :
302318 _log .warning (f"Shell command FAILED (exit code { res .exit_code } , see output above): { cmd_str } " )
303319 if fail_on_error :
304- raise_run_shell_cmd_error (res . cmd , res . exit_code , res . work_dir , output = res . output , stderr = res . stderr )
320+ raise_run_shell_cmd_error (res )
305321
306322 if with_hooks :
307323 run_hook_kwargs = {
@@ -319,15 +335,16 @@ def to_cmd_str(cmd):
319335 return res
320336
321337
322- def cmd_trace_msg (cmd , start_time , work_dir , stdin , cmd_out_fp ):
338+ def cmd_trace_msg (cmd , start_time , work_dir , stdin , cmd_out_fp , cmd_err_fp ):
323339 """
324340 Helper function to construct and print trace message for command being run
325341
326342 :param cmd: command being run
327343 :param start_time: datetime object indicating when command was started
328344 :param work_dir: path of working directory in which command is run
329345 :param stdin: stdin input value for command
330- :param cmd_out_fp: path to output log file for command
346+ :param cmd_out_fp: path to output file for command
347+ :param cmd_err_fp: path to errors/warnings output file for command
331348 """
332349 start_time = start_time .strftime ('%Y-%m-%d %H:%M:%S' )
333350
@@ -339,7 +356,9 @@ def cmd_trace_msg(cmd, start_time, work_dir, stdin, cmd_out_fp):
339356 if stdin :
340357 lines .append (f"\t [input: { stdin } ]" )
341358 if cmd_out_fp :
342- lines .append (f"\t [output logged in { cmd_out_fp } ]" )
359+ lines .append (f"\t [output saved to { cmd_out_fp } ]" )
360+ if cmd_err_fp :
361+ lines .append (f"\t [errors/warnings saved to { cmd_err_fp } ]" )
343362
344363 lines .append ('\t ' + cmd )
345364
0 commit comments