Skip to content

Commit

Permalink
Revert platformdirs on MacOS and Windows (#1297)
Browse files Browse the repository at this point in the history
* Revert platformdirs on MacOS and Windows

* Recommend moving to new default location

* Document recommendation to not use the old paths anymore

* Improve formatting

* log warning if there's a space in the pipx home

* switch from sys.platform to platform.system

* Improve wording

* Log warning if both fallback and default location exist

* Refactor fallback location handling

* Change logging level to info

* Improve docs
  • Loading branch information
Gitznik committed Mar 29, 2024
1 parent 9fafc97 commit 9679124
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 16 deletions.
4 changes: 4 additions & 0 deletions changelog.d/1257.feature.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 11 additions & 4 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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

Expand Down
5 changes: 5 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
14 changes: 12 additions & 2 deletions src/pipx/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
import sys
import platform
import sysconfig
from textwrap import dedent
from typing import NewType
Expand All @@ -26,14 +26,24 @@


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:
return sysconfig.get_platform().startswith("mingw")


WINDOWS: bool = is_windows()
MACOS: bool = is_macos()
LINUX: bool = is_linux()
MINGW: bool = is_mingw()

completion_instructions = dedent(
Expand Down
65 changes: 55 additions & 10 deletions src/pipx/paths.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
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"
DEFAULT_PIPX_GLOBAL_BIN_DIR = "/usr/local/bin"
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:
Expand All @@ -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
Expand All @@ -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")

Expand All @@ -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")

Expand All @@ -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)
Expand All @@ -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()

0 comments on commit 9679124

Please sign in to comment.