diff --git a/src/poetry/installation/wheel_installer.py b/src/poetry/installation/wheel_installer.py index 27a867f85bc..468bc915065 100644 --- a/src/poetry/installation/wheel_installer.py +++ b/src/poetry/installation/wheel_installer.py @@ -95,7 +95,7 @@ def install(self, wheel: Path) -> None: except _WheelFileValidationError as e: self.invalid_wheels[wheel] = e.issues - scheme_dict = self._env.paths.copy() + scheme_dict = self._env.installer_scheme_dict.copy() scheme_dict["headers"] = str( Path(scheme_dict["include"]) / source.distribution ) diff --git a/src/poetry/utils/env/base_env.py b/src/poetry/utils/env/base_env.py index 5f4d558a5f0..9e40c5eff77 100644 --- a/src/poetry/utils/env/base_env.py +++ b/src/poetry/utils/env/base_env.py @@ -17,6 +17,7 @@ from poetry.utils.env.exceptions import EnvCommandError from poetry.utils.env.site_packages import SitePackages from poetry.utils.helpers import get_real_windows_path +from poetry.utils.helpers import is_dir_writable if TYPE_CHECKING: @@ -55,6 +56,7 @@ def __init__(self, path: Path, base: Path | None = None) -> None: self._marker_env: dict[str, Any] | None = None self._site_packages: SitePackages | None = None self._paths: dict[str, str] | None = None + self._installer_scheme_dict: dict[str, str] | None = None self._supported_tags: list[Tag] | None = None self._purelib: Path | None = None self._platlib: Path | None = None @@ -242,6 +244,41 @@ def paths(self) -> dict[str, str]: return self._paths + @property + def installer_scheme_dict(self) -> dict[str, str]: + """ + This property exists to allow cases where system environment paths are not writable and + user site is enabled. This enables us to ensure packages (wheels) are correctly installed + into directories where the current user can write to. + """ + if self._installer_scheme_dict is None: + paths = self.paths.copy() + + if not self.is_venv() and paths.get("userbase"): + overrides: dict[str, str] = {} + base_path = os.path.commonpath([paths["include"], paths["purelib"]]) + + for key in [ + "include", + "purelib", + "platlib", + "stdlib", + "platinclude", + "scripts", + "data", + ]: + if not is_dir_writable(path=Path(paths[key]), create=True): + overrides[key] = paths[key].replace( + base_path, paths["userbase"] + ) + + for k, v in overrides.items(): + paths[k] = v + + self._installer_scheme_dict = paths + + return self._installer_scheme_dict + @property def supported_tags(self) -> list[Tag]: if self._supported_tags is None: @@ -312,7 +349,9 @@ def run_pip(self, *args: str, **kwargs: Any) -> str: def run_python_script(self, content: str, **kwargs: Any) -> str: return self.run( self._executable, - "-I", + # if isolation is enabled for system env, user site would be + # incorrectly disabled + *(["-I"] if self.is_venv() else []), "-W", "ignore", "-c",