diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 98c58bb5e35..1d5f15279e7 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -129,6 +129,7 @@ "tools.system.package_manager:mode": "Mode for package_manager tools: 'check', 'report', 'report-installed' or 'install'", "tools.system.package_manager:sudo": "Use 'sudo' when invoking the package manager tools in Linux (False by default)", "tools.system.package_manager:sudo_askpass": "Use the '-A' argument if using sudo in Linux to invoke the system package manager (False by default)", + "tools.system.pipenv:python_interpreter": "Path to the Python interpreter to be used to create the virtualenv", "tools.apple:sdk_path": "Path to the SDK to be used", "tools.apple:enable_bitcode": "(boolean) Enable/Disable Bitcode Apple Clang flags", "tools.apple:enable_arc": "(boolean) Enable/Disable ARC Apple Clang flags", diff --git a/conan/tools/system/pip_manager.py b/conan/tools/system/pip_manager.py index 214f613fad7..600ede2be82 100644 --- a/conan/tools/system/pip_manager.py +++ b/conan/tools/system/pip_manager.py @@ -1,9 +1,10 @@ -import venv import platform import os +import shutil from conan.tools.build import cmd_args_to_string from conan.tools.env.environment import Environment +from conan.errors import ConanException class PipEnv: @@ -23,6 +24,23 @@ def generate(self): env.prepend_path("PATH", self.bin_dir) env.vars(self._conanfile).save_script(self.env_name) + @staticmethod + def _default_python(): + default_python = shutil.which('python') if platform.system() == "Windows" else shutil.which('python3') + return os.path.realpath(default_python) if default_python else None + + def _create_venv(self): + python_interpreter = self._conanfile.conf.get("tools.system.pipenv:python_interpreter") or self._default_python() + if not python_interpreter: + raise ConanException("PipEnv could not find a Python executable path. " + "Please, install Python system-wide or set the 'tools.system.pipenv:python_interpreter' " + "conf to the full path of a Python executable") + + try: + self._conanfile.run(cmd_args_to_string([python_interpreter, '-m', 'venv', self._env_dir])) + except ConanException as e: + raise ConanException(f"PipEnv could not create a Python virtual environment using '{python_interpreter}': {e}") + def install(self, packages, pip_args=None): """ Will try to install the list of pip packages passed as a parameter. @@ -34,7 +52,7 @@ def install(self, packages, pip_args=None): :return: the return code of the executed pip command. """ - venv.EnvBuilder(clear=True, with_pip=True).create(self._env_dir) + self._create_venv() args = [self._python_exe, "-m", "pip", "install", "--disable-pip-version-check"] if pip_args: args += list(pip_args) diff --git a/test/integration/tools/system/pip_manager_test.py b/test/integration/tools/system/pip_manager_test.py new file mode 100644 index 00000000000..90b53fecea6 --- /dev/null +++ b/test/integration/tools/system/pip_manager_test.py @@ -0,0 +1,47 @@ +from conan.tools.system import PipEnv +from unittest.mock import patch +import pytest +from conan.errors import ConanException +from conan.internal.model.settings import Settings +from conan.test.utils.mocks import ConanFileMock + + +@patch('shutil.which') +def test_pipenv_conf(mock_shutil_which): + conanfile = ConanFileMock() + conanfile.settings = Settings() + conanfile.conf.define("tools.system.pipenv:python_interpreter", "/python/interpreter/from/config") + result = "/python/interpreter/from/config -m venv" + pipenv = PipEnv(conanfile, "testenv") + + def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, quiet=False): + assert result in command + return 100 + conanfile.run = fake_run + pipenv._create_venv() + mock_shutil_which.assert_not_called() + + +@patch('shutil.which') +def test_pipenv_error_message(mock_shutil_which): + conanfile = ConanFileMock() + conanfile.settings = Settings() + mock_shutil_which.return_value = None + with pytest.raises(ConanException) as exc_info: + pipenv = PipEnv(conanfile, "testenv") + pipenv._create_venv() + assert "install Python system-wide or set the 'tools.system.pipenv:python_interpreter' conf" in exc_info.value.args[0] + + +def test_pipenv_creation_error_message(): + conanfile = ConanFileMock() + conanfile.settings = Settings() + conanfile.conf.define("tools.system.pipenv:python_interpreter", "/python/interpreter/from/config") + pipenv = PipEnv(conanfile, "testenv") + + def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, quiet=False): + raise ConanException("fake error message") + conanfile.run = fake_run + with pytest.raises(ConanException) as exc_info: + pipenv._create_venv() + assert "using '/python/interpreter/from/config': fake error message" in exc_info.value.args[0]