diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f25414..a411bfb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `set_key` and `unset_key` no longer leave a `.tmp_*` file behind on Windows when writing a read-only `.env` fails, and the error raised is the one from the failed write rather than from cleaning up the temporary file by [@MohammedAlkindi] in [#686] - `load_dotenv`, `dotenv_values`, `get_key`, `set_key`, `unset_key` and the CLI `--file` option now expand a leading `~` to the user's home directory by [@veeceey] in [#615] - `find_dotenv` and the IPython `%dotenv` magic now expand a leading `~` in the file name by [@theskumar] in [#714] +- `dotenv run` on Windows now resolves the command against the `PATH` set in the `.env` file instead of ignoring it, so an activated virtualenv's interpreter is picked up by [@summeriver13] in [#730] ## [1.2.4] - 2026-10-01 @@ -467,6 +468,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [#700]: https://github.com/theskumar/python-dotenv/pull/700 [#711]: https://github.com/theskumar/python-dotenv/pull/711 [#714]: https://github.com/theskumar/python-dotenv/pull/714 +[#730]: https://github.com/theskumar/python-dotenv/pull/730 [790c5c0]: https://github.com/theskumar/python-dotenv/commit/790c5c02991100aa1bf41ee5330aca75edc51311 @@ -520,6 +522,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [@ShamikOfficial]: https://github.com/ShamikOfficial [@sidharth-sudhir]: https://github.com/sidharth-sudhir [@snobu]: https://github.com/snobu +[@summeriver13]: https://github.com/summeriver13 [@techalchemy]: https://github.com/techalchemy [@theGOTOguy]: https://github.com/theGOTOguy [@theskumar]: https://github.com/theskumar diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index c06c7068..8adc21f6 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -1,6 +1,7 @@ import json import os import shlex +import shutil import sys from contextlib import contextmanager from typing import IO, Any, Dict, Iterator, List, Optional @@ -237,9 +238,20 @@ def run_command(command: List[str], env: Dict[str, str]) -> None: if sys.platform == "win32": # execvpe on Windows returns control immediately # rather than once the command has finished. + # + # CreateProcess looks in the application directory before PATH, so a + # bare command name resolves to the executable that started this + # process rather than the first one on PATH (a virtualenv's python, + # for instance). Resolve it against the environment we are about to + # pass, which is not necessarily ours. try: p = Popen( - command, universal_newlines=True, bufsize=0, shell=False, env=cmd_env + command, + universal_newlines=True, + bufsize=0, + shell=False, + env=cmd_env, + executable=shutil.which(command[0], path=cmd_env.get("PATH")), ) except FileNotFoundError: print(f"Command not found: {command[0]}", file=sys.stderr) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8123c9dd..f4aab71d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -372,6 +372,33 @@ def test_run_with_env_missing_and_invalid_cmd(tmp_path): assert "Invalid value for '-f'" in result.stderr +def test_run_resolves_command_using_dotenv_path(tmp_path): + """ + Check that the command is resolved against the PATH built by `dotenv run`, + not the one of the calling process. + + The command below is only reachable through the PATH set in the .env file. + Windows' CreateProcess looks up executables using the environment of the + calling process and ignores the environment it is handed, so this used to + fail there while working everywhere else. + """ + + bindir = tmp_path / "bin" + bindir.mkdir() + if sys.platform == "win32": + (bindir / "dotenv-test-cmd.bat").write_text("@echo dotenv-test-ok\r\n") + else: + cmd = bindir / "dotenv-test-cmd" + cmd.write_text("#!/bin/sh\necho dotenv-test-ok\n") + cmd.chmod(0o755) + + (tmp_path / ".env").write_text(f"PATH={bindir}\n") + + result = run_dotenv(["run", "dotenv-test-cmd"], cwd=tmp_path) + + check_process(result, exit_code=0, stdout="dotenv-test-ok\n") + + def test_run_with_version(tmp_path): result = run_dotenv(["--version"], cwd=tmp_path)