Skip to content

Commit cf5177f

Browse files
authored
Merge pull request #4314 from boegel/run_systemtools
implement `fail_on_error`/`in_dry_run`/`output_file` options + enable caching for `run` function, and switch from `run_cmd` to `run` function in systemtools
2 parents 50457c3 + c045f23 commit cf5177f

File tree

4 files changed

+329
-149
lines changed

4 files changed

+329
-149
lines changed

easybuild/tools/run.py

+55-37
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,36 @@
7777
RunResult = namedtuple('RunResult', ('output', 'exit_code', 'stderr'))
7878

7979

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
80110
def run(cmd, fail_on_error=True, split_stderr=False, stdin=None,
81111
hidden=False, in_dry_run=False, work_dir=None, shell=True,
82112
output_file=False, stream_output=False, asynchronous=False,
@@ -90,7 +120,7 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None,
90120
:param hidden: do not show command in terminal output (when using --trace, or with --extended-dry-run / -x)
91121
:param in_dry_run: also run command in dry run mode
92122
: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)
94124
:param output_file: collect command output in temporary output file
95125
:param stream_output: stream command output to stdout
96126
:param asynchronous: run command asynchronously
@@ -104,7 +134,7 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None,
104134
"""
105135

106136
# 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)):
108138
raise NotImplementedError
109139

110140
if qa_patterns or qa_wait_patterns:
@@ -117,19 +147,24 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None,
117147
else:
118148
raise EasyBuildError(f"Unknown command type ('{type(cmd)}'): {cmd}")
119149

120-
silent = build_option('silent')
121-
122150
if work_dir is None:
123151
work_dir = os.getcwd()
124152

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
127161

128162
# 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'):
130164
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})"
133168
dry_run_msg(msg, silent=silent)
134169

135170
return RunResult(output='', exit_code=0, stderr=None)
@@ -142,14 +177,23 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None,
142177
# 'input' value fed to subprocess.run must be a byte sequence
143178
stdin = stdin.encode()
144179

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+
145188
_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)
147191

148192
# return output as a regular string rather than a byte sequence (and non-UTF-8 characters get stripped out)
149193
output = proc.stdout.decode('utf-8', 'ignore')
150194

151195
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}")
153197

154198
if not hidden:
155199
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):
185229
trace_msg('\n'.join(lines))
186230

187231

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-
214232
def get_output_from_process(proc, read_size=None, asynchronous=False):
215233
"""
216234
Get output from running process (that was opened with subprocess.Popen).

0 commit comments

Comments
 (0)