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 the detection of the system environment with custom installer #4433

Merged
merged 6 commits into from
Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions install-poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,10 @@ def make_env(self, version: str) -> Path:

virtualenv.cli_run([str(env_path), "--clear"])

# We add a special file so that Poetry can detect
# its own virtual environment
env_path.joinpath("poetry_env").touch()

return env_path

def make_bin(self, version: str) -> None:
Expand Down
14 changes: 11 additions & 3 deletions poetry/console/commands/env/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def _display_complete_info(self, env: "Env") -> None:
"<info>Path</info>: <comment>{}</>".format(
env.path if env.is_venv() else "NA"
),
"<info>Executable</info>: <comment>{}</>".format(
env.python if env.is_venv() else "NA"
),
]
if env.is_venv():
listing.append(
Expand All @@ -55,13 +58,18 @@ def _display_complete_info(self, env: "Env") -> None:

self.line("")

system_env = env.parent_env
self.line("<b>System</b>")
self.line(
"\n".join(
[
"<info>Platform</info>: <comment>{}</>".format(env.platform),
"<info>OS</info>: <comment>{}</>".format(env.os),
"<info>Python</info>: <comment>{}</>".format(env.base),
"<info>Platform</info>: <comment>{}</>".format(env.platform),
"<info>OS</info>: <comment>{}</>".format(env.os),
"<info>Python</info>: <comment>{}</>".format(
".".join(str(v) for v in system_env.version_info[:3])
),
"<info>Path</info>: <comment>{}</>".format(system_env.path),
"<info>Executable</info>: <comment>{}</>".format(system_env.python),
]
)
)
202 changes: 188 additions & 14 deletions poetry/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,38 @@ def _version_nodot(version):
print(json.dumps(sysconfig.get_paths()))
"""

GET_PATHS_FOR_GENERIC_ENVS = """\
# We can't use sysconfig.get_paths() because
# on some distributions it does not return the proper paths
# (those used by pip for instance). We go through distutils
# to get the proper ones.
import json
import site
import sysconfig

from distutils.command.install import SCHEME_KEYS # noqa
from distutils.core import Distribution

d = Distribution()
d.parse_config_files()
obj = d.get_command_obj("install", create=True)
obj.finalize_options()

paths = sysconfig.get_paths().copy()
for key in SCHEME_KEYS:
if key == "headers":
# headers is not a path returned by sysconfig.get_paths()
continue

paths[key] = getattr(obj, f"install_{key}")

if site.check_enableusersite() and hasattr(obj, "install_usersite"):
paths["usersite"] = getattr(obj, "install_usersite")
paths["userbase"] = getattr(obj, "install_userbase")

print(json.dumps(paths))
"""


class SitePackages:
def __init__(
Expand Down Expand Up @@ -730,7 +762,7 @@ def remove(self, python: str) -> "Env":

self.remove_venv(venv)

return VirtualEnv(venv)
return VirtualEnv(venv, venv)

def create_venv(
self,
Expand Down Expand Up @@ -1010,15 +1042,21 @@ def get_system_env(cls, naive: bool = False) -> Union["SystemEnv", "GenericEnv"]
(e.g. plugin installation or self update).
"""
prefix, base_prefix = Path(sys.prefix), Path(cls.get_base_prefix())
env = SystemEnv(prefix)
if not naive:
try:
Path(__file__).relative_to(prefix)
except ValueError:
pass
if prefix.joinpath("poetry_env").exists():
env = GenericEnv(base_prefix, child_env=env)
else:
return GenericEnv(base_prefix)
from poetry.locations import data_dir

return SystemEnv(prefix)
try:
prefix.relative_to(data_dir())
except ValueError:
pass
else:
env = GenericEnv(base_prefix, child_env=env)

return env

@classmethod
def get_base_prefix(cls) -> Path:
Expand Down Expand Up @@ -1056,6 +1094,11 @@ def __init__(self, path: Path, base: Optional[Path] = None) -> None:
self._path = path
self._bin_dir = self._path / bin_dir

self._executable = "python"
self._pip_executable = "pip"

self.find_executables()

self._base = base or path

self._marker_env = None
Expand Down Expand Up @@ -1090,7 +1133,7 @@ def python(self) -> str:
"""
Path to current python executable
"""
return self._bin("python")
return self._bin(self._executable)

@property
def marker_env(self) -> Dict[str, Any]:
Expand All @@ -1099,6 +1142,39 @@ def marker_env(self) -> Dict[str, Any]:

return self._marker_env

@property
def parent_env(self) -> "GenericEnv":
return GenericEnv(self.base, child_env=self)

def find_executables(self) -> None:
python_executables = sorted(
[
p.name
for p in self._bin_dir.glob("python*")
if re.match(r"python(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name)
]
)
if python_executables:
executable = python_executables[0]
if executable.endswith(".exe"):
executable = executable[:-4]

self._executable = executable

pip_executables = sorted(
[
p.name
for p in self._bin_dir.glob("pip*")
if re.match(r"pip(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name)
]
)
if pip_executables:
pip_executable = pip_executables[0]
if pip_executable.endswith(".exe"):
pip_executable = pip_executable[:-4]

self._pip_executable = pip_executable

def get_embedded_wheel(self, distribution):
return get_embed_wheel(
distribution, "{}.{}".format(self.version_info[0], self.version_info[1])
Expand All @@ -1116,7 +1192,7 @@ def pip(self) -> str:
Path to current pip executable
"""
# we do not use as_posix() here due to issues with windows pathlib2 implementation
path = self._bin("pip")
path = self._bin(self._pip_executable)
if not Path(path).exists():
return str(self.pip_embedded)
return path
Expand Down Expand Up @@ -1262,7 +1338,7 @@ def run_pip(self, *args: str, **kwargs: Any) -> Union[int, str]:
return self._run(cmd, **kwargs)

def run_python_script(self, content: str, **kwargs: Any) -> str:
return self.run("python", "-W", "ignore", "-", input_=content, **kwargs)
return self.run(self._executable, "-W", "ignore", "-", input_=content, **kwargs)

def _run(self, cmd: List[str], **kwargs: Any) -> Union[int, str]:
"""
Expand Down Expand Up @@ -1329,7 +1405,11 @@ def _bin(self, bin: str) -> str:
"""
Return path to the given executable.
"""
bin_path = (self._bin_dir / bin).with_suffix(".exe" if self._is_windows else "")
if self._is_windows and not bin.endswith(".exe"):
bin_path = self._bin_dir / (bin + ".exe")
else:
bin_path = self._bin_dir / bin

if not bin_path.exists():
# On Windows, some executables can be in the base path
# This is especially true when installing Python with
Expand All @@ -1340,7 +1420,11 @@ def _bin(self, bin: str) -> str:
# that creates a fake virtual environment pointing to
# a base Python install.
if self._is_windows:
bin_path = (self._path / bin).with_suffix(".exe")
if not bin.endswith(".exe"):
bin_path = self._bin_dir / (bin + ".exe")
else:
bin_path = self._path / bin

if bin_path.exists():
return str(bin_path)

Expand Down Expand Up @@ -1484,7 +1568,10 @@ def get_python_implementation(self) -> str:
def get_pip_command(self, embedded: bool = False) -> List[str]:
# We're in a virtualenv that is known to be sane,
# so assume that we have a functional pip
return [self._bin("python"), self.pip_embedded if embedded else self.pip]
return [
self._bin(self._executable),
self.pip_embedded if embedded else self.pip,
]

def get_supported_tags(self) -> List[Tag]:
file_path = Path(packaging.tags.__file__)
Expand Down Expand Up @@ -1585,6 +1672,90 @@ def _updated_path(self) -> str:


class GenericEnv(VirtualEnv):
def __init__(
self, path: Path, base: Optional[Path] = None, child_env: Optional["Env"] = None
) -> None:
self._child_env = child_env

super().__init__(path, base=base)

def find_executables(self) -> None:
patterns = [("python*", "pip*")]

if self._child_env:
minor_version = "{}.{}".format(
self._child_env.version_info[0], self._child_env.version_info[1]
)
major_version = "{}".format(self._child_env.version_info[0])
patterns = [
("python{}".format(minor_version), "pip{}".format(minor_version)),
("python{}".format(major_version), "pip{}".format(major_version)),
]

python_executable = None
pip_executable = None

for python_pattern, pip_pattern in patterns:
if python_executable and pip_executable:
break

if not python_executable:
python_executables = sorted(
[
p.name
for p in self._bin_dir.glob(python_pattern)
if re.match(r"python(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name)
]
)

if python_executables:
executable = python_executables[0]
if executable.endswith(".exe"):
executable = executable[:-4]

python_executable = executable

if not pip_executable:
pip_executables = sorted(
[
p.name
for p in self._bin_dir.glob(pip_pattern)
if re.match(r"pip(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name)
]
)
if pip_executables:
pip_executable = pip_executables[0]
if pip_executable.endswith(".exe"):
pip_executable = pip_executable[:-4]

pip_executable = pip_executable

if python_executable:
self._executable = python_executable

if pip_executable:
self._pip_executable = pip_executable

def get_paths(self) -> Dict[str, str]:
output = self.run_python_script(GET_PATHS_FOR_GENERIC_ENVS)

return json.loads(output)

def execute(self, bin: str, *args: str, **kwargs: Any) -> Optional[int]:
command = self.get_command_from_bin(bin) + list(args)
env = kwargs.pop("env", {k: v for k, v in os.environ.items()})

if not self._is_windows:
return os.execvpe(command[0], command, env=env)
else:
exe = subprocess.Popen([command[0]] + command[1:], env=env, **kwargs)
exe.communicate()

return exe.returncode

def _run(self, cmd: List[str], **kwargs: Any) -> Optional[int]:
return super(VirtualEnv, self)._run(cmd, **kwargs)

def is_venv(self) -> bool:
return self._path != self._base

Expand All @@ -1602,7 +1773,10 @@ def __init__(
self.executed = []

def get_pip_command(self, embedded: bool = False) -> List[str]:
return [self._bin("python"), self.pip_embedded if embedded else self.pip]
return [
self._bin(self._executable),
self.pip_embedded if embedded else self.pip,
]

def _run(self, cmd: List[str], **kwargs: Any) -> int:
self.executed.append(cmd)
Expand Down
17 changes: 13 additions & 4 deletions tests/console/commands/env/test_info.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sys

from pathlib import Path

import pytest
Expand Down Expand Up @@ -28,14 +30,21 @@ def test_env_info_displays_complete_info(tester):
Python: 3.7.0
Implementation: CPython
Path: {prefix}
Executable: {executable}
Valid: True

System
Platform: darwin
OS: posix
Python: {base_prefix}
Platform: darwin
OS: posix
Python: {base_version}
Path: {base_prefix}
Executable: {base_executable}
""".format(
prefix=str(Path("/prefix")), base_prefix=str(Path("/base/prefix"))
prefix=str(Path("/prefix")),
base_prefix=str(Path("/base/prefix")),
base_version=".".join(str(v) for v in sys.version_info[:3]),
executable=sys.executable,
base_executable="python",
)

assert expected == tester.io.fetch_output()
Expand Down
Loading