diff --git a/changelog.d/1257.feature.md b/changelog.d/1257.feature.md new file mode 100644 index 0000000000..ad6c9364d4 --- /dev/null +++ b/changelog.d/1257.feature.md @@ -0,0 +1,4 @@ +Revert platform-specific directories on MacOS and Windows + +They were leading to a lot of issues with Windows sandboxing +and spaces in shebangs on MacOS. diff --git a/docs/installation.md b/docs/installation.md index c90e4aca4d..52225cf8f1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -124,10 +124,14 @@ The default binary location for pipx-installed apps is `~/.local/bin`. This can variable `PIPX_BIN_DIR`. The default manual page location for pipx-installed apps is `~/.local/share/man`. This can be overridden with the environment variable `PIPX_MAN_DIR`. -pipx's default virtual environment location is typically `~/.local/share/pipx` on Linux/Unix, -`%USERPROFILE%\AppData\Local\pipx` on Windows and `~/Library/Application Support/pipx` on macOS, and for compatibility -reasons, if `~/.local/pipx` exists, it will be used as the default location instead. This can be overridden with the -`PIPX_HOME` environment variable. +pipx's default virtual environment location is typically `~/.local/share/pipx` on Linux/Unix, `~/.local/pipx` on MacOS +and `~\pipx` on Windows. For compatibility reasons, if `~/.local/pipx` on Linux, `%USERPROFILE%\AppData\Local\pipx` or +`~\.local\pipx` on Windows or `~/Library/Application Support/pipx` on MacOS exists, it will be used as the default location instead. +This can be overridden with the `PIPX_HOME` environment variable. + +In case one of these fallback locations exist, we recommend either manually moving the pipx files to the new default location +(see the `Troubleshooting` section of the docs), or setting the `PIPX_HOME` environment variable (discarding files existing in +the fallback location). As an example, you can install global apps accessible by all users on your system with the following command (on MacOS, Linux, and Windows WSL): @@ -152,6 +156,9 @@ sudo PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin PIPX_MAN_DIR=/usr/local/sha > > `user_data_dir()`, `user_cache_dir()` and `user_log_dir()` resolve to appropriate platform-specific user data, cache and log directories. > See the [platformdirs documentation](https://platformdirs.readthedocs.io/en/latest/api.html#platforms) for details. +> +> This was reverted in 1.5.0 for Windows and MacOS. We heavily recommend not using these locations on Windows and MacOS anymore, due to +> multiple incompatibilities discovered with these locations, documented [here](https://github.com/pypa/pipx/discussions/1247#discussion-6188916). ### Global installation diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index efef10cfbf..b4b6503bb8 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -143,6 +143,9 @@ data, cache, and log directories under it. To maintain compatibility with older this old `PIPX_HOME` path if it exists. For a map of old and new paths, see [Installation](installation.md#installation-options). +In Pipx version 1.5.0, this was reverted for Windows and MacOS. It defaults again to `~/.local/pipx` on MacOS and to +`~\pipx` on Windows. + If you have a `pipx` version later than 1.2.0 and want to migrate from the old path to the new paths, you can move the `~/.local/pipx` directory to the new location (after removing cache, log, and trash directories which will get recreated automatically) and then reinstall all packages. For example, on Linux systems, `PIPX_HOME` moves from `~/.local/pipx` to @@ -153,3 +156,5 @@ rm -rf ~/.local/pipx/{.cache,logs,trash} mkdir -p ~/.local/share && mv ~/.local/pipx ~/.local/share/ pipx reinstall-all ``` + +For moving the paths back after 1.5.0, you can perform the same steps, switching the paths around. diff --git a/src/pipx/constants.py b/src/pipx/constants.py index fa7a54d8b8..eec4f98dc8 100644 --- a/src/pipx/constants.py +++ b/src/pipx/constants.py @@ -1,5 +1,5 @@ import os -import sys +import platform import sysconfig from textwrap import dedent from typing import NewType @@ -26,7 +26,15 @@ def is_windows() -> bool: - return sys.platform == "win32" + return platform.system() == "Windows" + + +def is_macos() -> bool: + return platform.system() == "Darwin" + + +def is_linux() -> bool: + return platform.system() == "Linux" def is_mingw() -> bool: @@ -34,6 +42,8 @@ def is_mingw() -> bool: WINDOWS: bool = is_windows() +MACOS: bool = is_macos() +LINUX: bool = is_linux() MINGW: bool = is_mingw() completion_instructions = dedent( diff --git a/src/pipx/paths.py b/src/pipx/paths.py index 7f790003a0..cbb9c38289 100644 --- a/src/pipx/paths.py +++ b/src/pipx/paths.py @@ -1,11 +1,24 @@ +import logging import os from pathlib import Path -from typing import Optional, Union +from typing import List, Optional, Union from platformdirs import user_cache_path, user_data_path, user_log_path -DEFAULT_PIPX_HOME = user_data_path("pipx") -FALLBACK_PIPX_HOME = Path.home() / ".local/pipx" +from pipx.constants import LINUX, WINDOWS +from pipx.emojis import hazard +from pipx.util import pipx_wrap + +if LINUX: + DEFAULT_PIPX_HOME = user_data_path("pipx") + FALLBACK_PIPX_HOMES = [Path.home() / ".local/pipx"] +elif WINDOWS: + DEFAULT_PIPX_HOME = Path.home() / "pipx" + FALLBACK_PIPX_HOMES = [Path.home() / ".local/pipx", user_data_path("pipx")] +else: + DEFAULT_PIPX_HOME = Path.home() / ".local/pipx" + FALLBACK_PIPX_HOMES = [user_data_path("pipx")] + DEFAULT_PIPX_BIN_DIR = Path.home() / ".local/bin" DEFAULT_PIPX_MAN_DIR = Path.home() / ".local/share/man" DEFAULT_PIPX_GLOBAL_HOME = "/opt/pipx" @@ -13,6 +26,9 @@ DEFAULT_PIPX_GLOBAL_MAN_DIR = "/usr/local/share/man" +logger = logging.getLogger(__name__) + + def get_expanded_environ(env_name: str) -> Optional[Path]: val = os.environ.get(env_name) if val is not None: @@ -25,8 +41,9 @@ class _PathContext: _base_bin: Optional[Union[Path, str]] = get_expanded_environ("PIPX_BIN_DIR") _base_man: Optional[Union[Path, str]] = get_expanded_environ("PIPX_MAN_DIR") _base_shared_libs: Optional[Union[Path, str]] = get_expanded_environ("PIPX_SHARED_LIBS") - _fallback_home: Path = Path.home() / ".local/pipx" - _home_exists: bool = _base_home is not None or _fallback_home.exists() + _fallback_homes: List[Path] = FALLBACK_PIPX_HOMES + _fallback_home: Optional[Path] = next(iter([fallback for fallback in _fallback_homes if fallback.exists()]), None) + _home_exists: bool = _base_home is not None or any(fallback.exists() for fallback in _fallback_homes) log_file: Optional[Path] = None @property @@ -35,7 +52,7 @@ def venvs(self) -> Path: @property def logs(self) -> Path: - if self._home_exists: + if self._home_exists or not LINUX: return self.home / "logs" return user_log_path("pipx") @@ -47,7 +64,7 @@ def trash(self) -> Path: @property def venv_cache(self) -> Path: - if self._home_exists: + if self._home_exists or not LINUX: return self.home / ".cache" return user_cache_path("pipx") @@ -63,7 +80,7 @@ def man_dir(self) -> Path: def home(self) -> Path: if self._base_home: home = Path(self._base_home) - elif self._fallback_home.exists(): + elif self._fallback_home: home = self._fallback_home else: home = Path(DEFAULT_PIPX_HOME) @@ -77,17 +94,45 @@ def make_local(self) -> None: self._base_home = get_expanded_environ("PIPX_HOME") self._base_bin = get_expanded_environ("PIPX_BIN_DIR") self._base_man = get_expanded_environ("PIPX_MAN_DIR") - self._home_exists = self._base_home is not None or self._fallback_home.exists() + self._home_exists = self._base_home is not None or any(fallback.exists() for fallback in self._fallback_homes) def make_global(self) -> None: self._base_home = get_expanded_environ("PIPX_GLOBAL_HOME") or DEFAULT_PIPX_GLOBAL_HOME self._base_bin = get_expanded_environ("PIPX_GLOBAL_BIN_DIR") or DEFAULT_PIPX_GLOBAL_BIN_DIR self._base_man = get_expanded_environ("PIPX_GLOBAL_MAN_DIR") or DEFAULT_PIPX_GLOBAL_MAN_DIR - self._home_exists = self._base_home is not None or self._fallback_home.exists() + self._home_exists = self._base_home is not None or any(fallback.exists() for fallback in self._fallback_homes) @property def standalone_python_cachedir(self) -> Path: return self.home / "py" + def log_warnings(self): + if " " in str(self.home): + logger.warning( + pipx_wrap( + ( + f"{hazard} Found a space in the home path. We heavily discourage this, due to " + "multiple incompatibilities. Please check our docs for more information on this, " + "as well as some pointers on how to migrate to a different home path." + ), + subsequent_indent=" " * 4, + ) + ) + + fallback_home_exists = self._fallback_home is not None and self._fallback_home.exists() + specific_home_exists = self.home != self._fallback_home + if fallback_home_exists and specific_home_exists: + logger.info( + pipx_wrap( + ( + f"Both a specific pipx home folder ({self.home}) and the fallback " + f"pipx home folder ({self._fallback_home}) exist. If you are done migrating from the" + "fallback to the new location, it is safe to delete the fallback location." + ), + subsequent_indent=" " * 4, + ) + ) + ctx = _PathContext() +ctx.log_warnings()