diff --git a/autotest/test_mbase.py b/autotest/test_mbase.py index 59e796b05..f250d9a65 100644 --- a/autotest/test_mbase.py +++ b/autotest/test_mbase.py @@ -135,10 +135,8 @@ def test_run_model_exe_rel_path(mf6_model_path, function_tmpdir, use_ext): bin_dir = function_tmpdir / "bin" bin_dir.mkdir() - inner_dir = function_tmpdir / "inner" - inner_dir.mkdir() - with set_dir(inner_dir): + with set_dir(ws): # copy exe to relative dir copy(mf6, bin_dir / "mf6") assert (bin_dir / "mf6").is_file() diff --git a/flopy/mbase.py b/flopy/mbase.py index c662854d0..02c6ad2dd 100644 --- a/flopy/mbase.py +++ b/flopy/mbase.py @@ -64,31 +64,31 @@ def resolve_exe(exe_name: Union[str, os.PathLike], forgive: bool = False) -> str str: absolute path to the executable """ - def _resolve(exe_name): - exe = which(exe_name) - if exe is not None: - # if which() returned a relative path, resolve it - exe = which(str(Path(exe).resolve())) - else: - if exe_name.lower().endswith(".exe"): - # try removing .exe suffix - exe = which(exe_name[:-4]) - if exe is not None: - # in case which() returned a relative path, resolve it - exe = which(str(Path(exe).resolve())) - else: - # try tilde-expanded abspath - exe = which(Path(exe_name).expanduser().absolute()) - if exe is None and exe_name.lower().endswith(".exe"): - # try tilde-expanded abspath without .exe suffix - exe = which(Path(exe_name[:-4]).expanduser().absolute()) - return exe - - name = str(exe_name) - exe_path = _resolve(name) - if exe_path is None and on_windows and Path(name).suffix == "": - # try adding .exe suffix on windows (for portability from other OS) - exe_path = _resolve(f"{name}.exe") + def _resolve(exe_name, checked=set()): + # Prevent infinite recursion by checking if exe_name has been checked + if exe_name in checked: + return None + checked.add(exe_name) + + # exe_name is found (not None), ensure absolute path is returned + if exe := which(str(exe_name)): + return which(str(Path(exe).resolve())) + + # exe_name is relative path + if not Path(exe_name).is_absolute() and ( + exe := which(str(Path(exe_name).expanduser().resolve()), mode=0) + ): + # expanduser() in case of ~ in path + # mode=0 effectively allows which() to find exe without suffix in windows + return exe + + # try adding/removing .exe suffix + if exe_name.lower().endswith(".exe"): + return _resolve(exe_name[:-4], checked) + elif on_windows and "." not in Path(exe_name).stem: + return _resolve(f"{exe_name}.exe", checked) + + exe_path = _resolve(exe_name) # raise if we are unforgiving, otherwise return None if exe_path is None: @@ -103,7 +103,7 @@ def _resolve(exe_name): f"The program {exe_name} does not exist or is not executable." ) - return exe_path + return str(exe_path) # external exceptions for users