diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index 544e5fe78ae..b0989197db5 100644 --- a/src/poetry/utils/env.py +++ b/src/poetry/utils/env.py @@ -1429,6 +1429,7 @@ def execute(self, bin: str, *args: str, **kwargs: Any) -> int | None: if not self._is_windows: return os.execvpe(command[0], command, env=env) + kwargs["shell"] = True exe = subprocess.Popen([command[0]] + command[1:], env=env, **kwargs) exe.communicate() return exe.returncode diff --git a/tests/console/commands/test_run.py b/tests/console/commands/test_run.py index 2c52bebeabe..d86e85523a9 100644 --- a/tests/console/commands/test_run.py +++ b/tests/console/commands/test_run.py @@ -4,6 +4,8 @@ import pytest +from poetry.utils._compat import WINDOWS + if TYPE_CHECKING: from cleo.testers.application_tester import ApplicationTester @@ -11,6 +13,7 @@ from pytest_mock import MockerFixture from poetry.utils.env import MockEnv + from poetry.utils.env import VirtualEnv from tests.types import CommandTesterFactory @@ -42,11 +45,61 @@ def test_run_keeps_options_passed_before_command( def test_run_has_helpful_error_when_command_not_found( - app_tester: ApplicationTester, env: MockEnv + app_tester: ApplicationTester, env: MockEnv, capfd: pytest.CaptureFixture[str] ): env._execute = True app_tester.execute("run nonexistent-command") assert env.executed == [["nonexistent-command"]] assert app_tester.status_code == 1 - assert app_tester.io.fetch_error() == "Command not found: nonexistent-command\n" + if WINDOWS: + # On Windows we use a shell to run commands which provides its own error + # message when a command is not found that is not captured by the + # ApplicationTester but is captured by pytest, and we can access it via capfd. + # The expected string in this assertion assumes Command Prompt (cmd.exe) is the + # shell used. + assert capfd.readouterr().err.splitlines() == [ + "'nonexistent-command' is not recognized as an internal or external" + " command,", + "operable program or batch file.", + ] + else: + assert app_tester.io.fetch_error() == "Command not found: nonexistent-command\n" + + +@pytest.mark.skipif( + not WINDOWS, + reason=( + "Poetry only installs CMD script files for console scripts of editable" + " dependencies on Windows" + ), +) +def test_run_console_scripts_of_editable_dependencies_on_windows( + tmp_venv: VirtualEnv, + command_tester_factory: CommandTesterFactory, +): + """ + On Windows, Poetry installs console scripts of editable dependencies by creating + in the environment's `Scripts/` directory both: + + A) a Python file named after the console script (no `.py` extension) which + imports and calls the console script using Python code + B) a CMD script file also named after the console script + (with `.cmd` extension) which calls `python.exe` to execute (A) + + This configuration enables calling the console script by name from `cmd.exe` + because the `.cmd` file extension appears by default in the PATHEXT environment + variable that `cmd.exe` uses to determine which file should be executed if a + filename without an extension is executed as a command. + + This test validates that you can also run such a CMD script file via `poetry run` + just by providing the script's name without the `.cmd` extension. + """ + tester = command_tester_factory("run", environment=tmp_venv) + + cmd_script_file = tmp_venv._bin_dir / "quix.cmd" + # `/b` ensures we only exit the script instead of any cmd.exe proc that called it + cmd_script_file.write_text("exit /b 123") + # We prove that the CMD script executed successfully by verifying the exit code + # matches what we wrote in the script + assert tester.execute("quix") == 123