Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(resolve_exe): allow shutil.which() to find exe without suffix in Windows #2408

Merged
merged 8 commits into from
Jan 10, 2025
4 changes: 1 addition & 3 deletions autotest/test_mbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
52 changes: 26 additions & 26 deletions flopy/mbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
Loading