77
77
RunResult = namedtuple ('RunResult' , ('output' , 'exit_code' , 'stderr' ))
78
78
79
79
80
+ def run_cmd_cache (func ):
81
+ """Function decorator to cache (and retrieve cached) results of running commands."""
82
+ cache = {}
83
+
84
+ @functools .wraps (func )
85
+ def cache_aware_func (cmd , * args , ** kwargs ):
86
+ """Retrieve cached result of selected commands, or run specified and collect & cache result."""
87
+ # cache key is combination of command and input provided via stdin ('stdin' for run, 'inp' for run_cmd)
88
+ key = (cmd , kwargs .get ('stdin' , None ) or kwargs .get ('inp' , None ))
89
+ # fetch from cache if available, cache it if it's not, but only on cmd strings
90
+ if isinstance (cmd , str ) and key in cache :
91
+ _log .debug ("Using cached value for command '%s': %s" , cmd , cache [key ])
92
+ return cache [key ]
93
+ else :
94
+ res = func (cmd , * args , ** kwargs )
95
+ if cmd in CACHED_COMMANDS :
96
+ cache [key ] = res
97
+ return res
98
+
99
+ # expose clear/update methods of cache to wrapped function
100
+ cache_aware_func .clear_cache = cache .clear
101
+ cache_aware_func .update_cache = cache .update
102
+
103
+ return cache_aware_func
104
+
105
+
106
+ run_cache = run_cmd_cache
107
+
108
+
109
+ @run_cache
80
110
def run (cmd , fail_on_error = True , split_stderr = False , stdin = None ,
81
111
hidden = False , in_dry_run = False , work_dir = None , shell = True ,
82
112
output_file = False , stream_output = False , asynchronous = False ,
@@ -90,7 +120,7 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None,
90
120
:param hidden: do not show command in terminal output (when using --trace, or with --extended-dry-run / -x)
91
121
:param in_dry_run: also run command in dry run mode
92
122
:param work_dir: working directory to run command in (current working directory if None)
93
- :param shell: execute command through a shell (enabled by default)
123
+ :param shell: execute command through bash shell (enabled by default)
94
124
:param output_file: collect command output in temporary output file
95
125
:param stream_output: stream command output to stdout
96
126
:param asynchronous: run command asynchronously
@@ -104,7 +134,7 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None,
104
134
"""
105
135
106
136
# temporarily raise a NotImplementedError until all options are implemented
107
- if any ((not fail_on_error , split_stderr , in_dry_run , work_dir , output_file , stream_output , asynchronous )):
137
+ if any ((split_stderr , work_dir , stream_output , asynchronous )):
108
138
raise NotImplementedError
109
139
110
140
if qa_patterns or qa_wait_patterns :
@@ -117,19 +147,24 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None,
117
147
else :
118
148
raise EasyBuildError (f"Unknown command type ('{ type (cmd )} '): { cmd } " )
119
149
120
- silent = build_option ('silent' )
121
-
122
150
if work_dir is None :
123
151
work_dir = os .getcwd ()
124
152
125
- # output file for command output (only used if output_file is enabled)
126
- cmd_out_fp = None
153
+ # temporary output file for command output, if requested
154
+ if output_file or not hidden :
155
+ # collect output of running command in temporary log file, if desired
156
+ fd , cmd_out_fp = tempfile .mkstemp (suffix = '.log' , prefix = 'easybuild-run-' )
157
+ os .close (fd )
158
+ _log .info (f'run_cmd: Output of "{ cmd } " will be logged to { cmd_out_fp } ' )
159
+ else :
160
+ cmd_out_fp = None
127
161
128
162
# early exit in 'dry run' mode, after printing the command that would be run (unless 'hidden' is enabled)
129
- if build_option ('extended_dry_run' ):
163
+ if not in_dry_run and build_option ('extended_dry_run' ):
130
164
if not hidden :
131
- msg = f" running command \" %{ cmd_msg } s\" \n "
132
- msg += f" (in %{ work_dir } )"
165
+ silent = build_option ('silent' )
166
+ msg = f" running command \" { cmd_msg } s\" \n "
167
+ msg += f" (in { work_dir } )"
133
168
dry_run_msg (msg , silent = silent )
134
169
135
170
return RunResult (output = '' , exit_code = 0 , stderr = None )
@@ -142,14 +177,23 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None,
142
177
# 'input' value fed to subprocess.run must be a byte sequence
143
178
stdin = stdin .encode ()
144
179
180
+ # use bash as shell instead of the default /bin/sh used by subprocess.run
181
+ # (which could be dash instead of bash, like on Ubuntu, see https://wiki.ubuntu.com/DashAsBinSh)
182
+ if shell :
183
+ executable = '/bin/bash'
184
+ else :
185
+ # stick to None (default value) when not running command via a shell
186
+ executable = None
187
+
145
188
_log .info (f"Running command '{ cmd_msg } ' in { work_dir } " )
146
- proc = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , input = stdin , shell = shell )
189
+ proc = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , check = fail_on_error ,
190
+ input = stdin , shell = shell , executable = executable )
147
191
148
192
# return output as a regular string rather than a byte sequence (and non-UTF-8 characters get stripped out)
149
193
output = proc .stdout .decode ('utf-8' , 'ignore' )
150
194
151
195
res = RunResult (output = output , exit_code = proc .returncode , stderr = None )
152
- _log .info (f"Command '{ cmd_msg } ' exited with exit code { res .exit_code } and output:\n % { res .output } " )
196
+ _log .info (f"Command '{ cmd_msg } ' exited with exit code { res .exit_code } and output:\n { res .output } " )
153
197
154
198
if not hidden :
155
199
time_since_start = time_str_since (start_time )
@@ -185,32 +229,6 @@ def cmd_trace_msg(cmd, start_time, work_dir, stdin, cmd_out_fp):
185
229
trace_msg ('\n ' .join (lines ))
186
230
187
231
188
- def run_cmd_cache (func ):
189
- """Function decorator to cache (and retrieve cached) results of running commands."""
190
- cache = {}
191
-
192
- @functools .wraps (func )
193
- def cache_aware_func (cmd , * args , ** kwargs ):
194
- """Retrieve cached result of selected commands, or run specified and collect & cache result."""
195
- # cache key is combination of command and input provided via stdin
196
- key = (cmd , kwargs .get ('inp' , None ))
197
- # fetch from cache if available, cache it if it's not, but only on cmd strings
198
- if isinstance (cmd , str ) and key in cache :
199
- _log .debug ("Using cached value for command '%s': %s" , cmd , cache [key ])
200
- return cache [key ]
201
- else :
202
- res = func (cmd , * args , ** kwargs )
203
- if cmd in CACHED_COMMANDS :
204
- cache [key ] = res
205
- return res
206
-
207
- # expose clear/update methods of cache to wrapped function
208
- cache_aware_func .clear_cache = cache .clear
209
- cache_aware_func .update_cache = cache .update
210
-
211
- return cache_aware_func
212
-
213
-
214
232
def get_output_from_process (proc , read_size = None , asynchronous = False ):
215
233
"""
216
234
Get output from running process (that was opened with subprocess.Popen).
0 commit comments