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

Revert platformdirs on MacOS and Windows #1297

Merged
merged 15 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
and `~/.local/pipx` on Windows and MacOS. For compatibility reasons, if `~/.local/pipx` on Linux, `%USERPROFILE%\AppData\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.
dukecat0 marked this conversation as resolved.
Show resolved Hide resolved

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
4 changes: 4 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ 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`.

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 +155,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
60 changes: 51 additions & 9 deletions src/pipx/paths.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
import logging
import os
from pathlib import Path
from typing import 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.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 +40,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
chrysle marked this conversation as resolved.
Show resolved Hide resolved
_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 +51,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 +63,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 +79,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 +93,43 @@ 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(
(
":hazard: Found a space in the home path. We heavily discourage this, due to "
chrysle marked this conversation as resolved.
Show resolved Hide resolved
"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,
)
)

if self._fallback_homes and DEFAULT_PIPX_HOME.exists():
chrysle marked this conversation as resolved.
Show resolved Hide resolved
logger.info(
pipx_wrap(
(
f"Both the default pipx home folder ({DEFAULT_PIPX_HOME}) and the fallback "
f"pipx home folder ({self._fallback_homes}) exist. If you are done migrating from the"
chrysle marked this conversation as resolved.
Show resolved Hide resolved
"fallback to the new default, it is safe to delete the fallback location."
),
subsequent_indent=" " * 4,
)
)


ctx = _PathContext()
ctx.log_warnings()