Skip to content

Commit

Permalink
Fix #1475 (--global installs to ~/.local) (#1476)
Browse files Browse the repository at this point in the history
Co-authored-by: chrysle <96722107+chrysle@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 30, 2024
1 parent eb71c12 commit 2a7345a
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 53 deletions.
1 change: 1 addition & 0 deletions changelog.d/1475.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Stop `pipx install --global` from installing files in `~/.local`.
6 changes: 4 additions & 2 deletions src/pipx/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,7 @@ def package_is_path(package: str):

def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.ArgumentParser]) -> ExitCode: # noqa: C901
verbose = args.verbose if "verbose" in args else False
if not constants.WINDOWS and args.is_global:
paths.ctx.make_global()

pip_args = get_pip_args(vars(args))
venv_args = get_venv_args(vars(args))

Expand Down Expand Up @@ -1087,6 +1086,9 @@ def setup(args: argparse.Namespace) -> None:
print_version()
sys.exit(0)

if not constants.WINDOWS and args.is_global:
paths.ctx.make_global()

verbose = args.verbose - args.quiet

setup_logging(verbose)
Expand Down
92 changes: 62 additions & 30 deletions src/pipx/paths.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import os
from pathlib import Path
from typing import List, Optional, Union
from typing import Optional

from platformdirs import user_cache_path, user_data_path, user_log_path

Expand All @@ -10,21 +10,29 @@
from pipx.util import pipx_wrap

if LINUX:
DEFAULT_PIPX_HOME = user_data_path("pipx")
DEFAULT_PIPX_HOME = Path(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")]
FALLBACK_PIPX_HOMES = [Path.home() / ".local/pipx", Path(user_data_path("pipx"))]
else:
DEFAULT_PIPX_HOME = Path.home() / ".local/pipx"
FALLBACK_PIPX_HOMES = [user_data_path("pipx")]
FALLBACK_PIPX_HOMES = [Path(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"

DEFAULT_PIPX_GLOBAL_HOME = Path("/opt/pipx")
DEFAULT_PIPX_GLOBAL_BIN_DIR = Path("/usr/local/bin")
DEFAULT_PIPX_GLOBAL_MAN_DIR = Path("/usr/local/share/man")

# Overrides for testing
OVERRIDE_PIPX_HOME = None
OVERRIDE_PIPX_BIN_DIR = None
OVERRIDE_PIPX_MAN_DIR = None
OVERRIDE_PIPX_SHARED_LIBS = None
OVERRIDE_PIPX_GLOBAL_HOME = None
OVERRIDE_PIPX_GLOBAL_BIN_DIR = None
OVERRIDE_PIPX_GLOBAL_MAN_DIR = None

logger = logging.getLogger(__name__)

Expand All @@ -37,15 +45,23 @@ def get_expanded_environ(env_name: str) -> Optional[Path]:


class _PathContext:
_base_home: Optional[Union[Path, str]] = get_expanded_environ("PIPX_HOME")
_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_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)
_base_home: Optional[Path]
_default_home: Path
_base_bin: Optional[Path]
_default_bin: Path
_base_man: Optional[Path]
_default_man: Path
_default_log: Path
_default_cache: Path
_default_trash: Path
_base_shared_libs: Optional[Path]
_fallback_home: Optional[Path]
_home_exists: bool
log_file: Optional[Path] = None

def __init__(self):
self.make_local()

@property
def venvs(self) -> Path:
return self.home / "venvs"
Expand All @@ -54,27 +70,27 @@ def venvs(self) -> Path:
def logs(self) -> Path:
if self._home_exists or not LINUX:
return self.home / "logs"
return user_log_path("pipx")
return self._default_log

@property
def trash(self) -> Path:
if self._home_exists:
return self.home / ".trash"
return self.home / "trash"
return self._default_trash

@property
def venv_cache(self) -> Path:
if self._home_exists or not LINUX:
return self.home / ".cache"
return user_cache_path("pipx")
return self._default_cache

@property
def bin_dir(self) -> Path:
return Path(self._base_bin or DEFAULT_PIPX_BIN_DIR).resolve()
return (self._base_bin or self._default_bin).resolve()

@property
def man_dir(self) -> Path:
return Path(self._base_man or DEFAULT_PIPX_MAN_DIR).resolve()
return (self._base_man or self._default_man).resolve()

@property
def home(self) -> Path:
Expand All @@ -83,24 +99,40 @@ def home(self) -> Path:
elif self._fallback_home:
home = self._fallback_home
else:
home = Path(DEFAULT_PIPX_HOME)
home = self._default_home
return home.resolve()

@property
def shared_libs(self) -> Path:
return Path(self._base_shared_libs or self.home / "shared").resolve()
return (self._base_shared_libs or self.home / "shared").resolve()

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 any(fallback.exists() for fallback in self._fallback_homes)
self._base_home = OVERRIDE_PIPX_HOME or get_expanded_environ("PIPX_HOME")
self._default_home = DEFAULT_PIPX_HOME
self._base_bin = OVERRIDE_PIPX_BIN_DIR or get_expanded_environ("PIPX_BIN_DIR")
self._default_bin = DEFAULT_PIPX_BIN_DIR
self._base_man = OVERRIDE_PIPX_MAN_DIR or get_expanded_environ("PIPX_MAN_DIR")
self._default_man = DEFAULT_PIPX_MAN_DIR
self._base_shared_libs = OVERRIDE_PIPX_SHARED_LIBS or get_expanded_environ("PIPX_SHARED_LIBS")
self._default_log = Path(user_log_path("pipx"))
self._default_cache = Path(user_cache_path("pipx"))
self._default_trash = self._default_home / "trash"
self._fallback_home = next(iter([fallback for fallback in FALLBACK_PIPX_HOMES if fallback.exists()]), None)
self._home_exists = self._base_home is not None or any(fallback.exists() for fallback in FALLBACK_PIPX_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 any(fallback.exists() for fallback in self._fallback_homes)
self._base_home = OVERRIDE_PIPX_GLOBAL_HOME or get_expanded_environ("PIPX_GLOBAL_HOME")
self._default_home = DEFAULT_PIPX_GLOBAL_HOME
self._base_bin = OVERRIDE_PIPX_GLOBAL_BIN_DIR or get_expanded_environ("PIPX_GLOBAL_BIN_DIR")
self._default_bin = DEFAULT_PIPX_GLOBAL_BIN_DIR
self._base_man = OVERRIDE_PIPX_GLOBAL_MAN_DIR or get_expanded_environ("PIPX_GLOBAL_MAN_DIR")
self._default_man = DEFAULT_PIPX_GLOBAL_MAN_DIR
self._default_log = self._default_home / "logs"
self._default_cache = self._default_home / ".cache"
self._default_trash = self._default_home / "trash"
self._base_shared_libs = None
self._fallback_home = None
self._home_exists = self._base_home is not None

@property
def standalone_python_cachedir(self) -> Path:
Expand Down
40 changes: 28 additions & 12 deletions src/pipx/shared_libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
import time
from pathlib import Path
from typing import List, Optional
from typing import Dict, List

from pipx import paths
from pipx.animate import animate
Expand All @@ -23,21 +23,39 @@

class _SharedLibs:
def __init__(self) -> None:
self.root = paths.ctx.shared_libs
self.bin_path, self.python_path, self.man_path = get_venv_paths(self.root)
self.pip_path = self.bin_path / ("pip" if not WINDOWS else "pip.exe")
# i.e. bin_path is ~/.local/share/pipx/shared/bin
# i.e. python_path is ~/.local/share/pipx/shared/python
self._site_packages: Optional[Path] = None
self._site_packages: Dict[Path, Path] = {}
self.has_been_updated_this_run = False
self.has_been_logged_this_run = False

@property
def root(self) -> Path:
return paths.ctx.shared_libs

@property
def bin_path(self) -> Path:
bin_path, _, _ = get_venv_paths(self.root)
return bin_path

@property
def python_path(self) -> Path:
_, python_path, _ = get_venv_paths(self.root)
return python_path

@property
def man_path(self) -> Path:
_, _, man_path = get_venv_paths(self.root)
return man_path

@property
def pip_path(self) -> Path:
return self.bin_path / ("pip" if not WINDOWS else "pip.exe")

@property
def site_packages(self) -> Path:
if self._site_packages is None:
self._site_packages = get_site_packages(self.python_path)
if self.python_path not in self._site_packages:
self._site_packages[self.python_path] = get_site_packages(self.python_path)

return self._site_packages
return self._site_packages[self.python_path]

def create(self, pip_args: List[str], verbose: bool = False) -> None:
if not self.is_valid:
Expand All @@ -46,8 +64,6 @@ def create(self, pip_args: List[str], verbose: bool = False) -> None:
[DEFAULT_PYTHON, "-m", "venv", "--clear", self.root], run_dir=str(self.root)
)
subprocess_post_check(create_process)
# Recompute these paths, as they might resolve differently now, see comment in get_venv_paths
self.bin_path, self.python_path, self.man_path = get_venv_paths(self.root)

# ignore installed packages to ensure no unexpected patches from the OS vendor
# are used
Expand Down
21 changes: 12 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,18 @@ def pipx_temp_env_helper(pipx_shared_dir, tmp_path, monkeypatch, request, utils_
global_bin_dir = Path(tmp_path) / "global_otherdir" / "pipxbindir"
global_man_dir = Path(tmp_path) / "global_otherdir" / "pipxmandir"

# Patch in test specific base paths
monkeypatch.setattr(paths.ctx, "_base_shared_libs", pipx_shared_dir)
monkeypatch.setattr(paths.ctx, "_base_home", home_dir)
monkeypatch.setattr(paths.ctx, "_base_bin", bin_dir)
monkeypatch.setattr(paths.ctx, "_base_man", man_dir)
# Patch the default global paths so developers don't contaminate their own systems
monkeypatch.setattr(paths, "DEFAULT_PIPX_GLOBAL_BIN_DIR", global_bin_dir)
monkeypatch.setattr(paths, "DEFAULT_PIPX_GLOBAL_HOME", global_home_dir)
monkeypatch.setattr(paths, "DEFAULT_PIPX_GLOBAL_MAN_DIR", global_man_dir)
# Patch in test specific paths
monkeypatch.setattr(paths, "OVERRIDE_PIPX_HOME", home_dir)
monkeypatch.setattr(paths, "OVERRIDE_PIPX_BIN_DIR", bin_dir)
monkeypatch.setattr(paths, "OVERRIDE_PIPX_MAN_DIR", man_dir)
monkeypatch.setattr(paths, "OVERRIDE_PIPX_SHARED_LIBS", pipx_shared_dir)
monkeypatch.setattr(paths, "OVERRIDE_PIPX_GLOBAL_HOME", global_home_dir)
monkeypatch.setattr(paths, "OVERRIDE_PIPX_GLOBAL_BIN_DIR", global_bin_dir)
monkeypatch.setattr(paths, "OVERRIDE_PIPX_GLOBAL_MAN_DIR", global_man_dir)
# Refresh paths.ctx to commit the overrides
paths.ctx.make_local()

# Reset internal state of shared_libs
monkeypatch.setattr(shared_libs, "shared_libs", shared_libs._SharedLibs())
monkeypatch.setattr(venv, "shared_libs", shared_libs.shared_libs)

Expand Down

0 comments on commit 2a7345a

Please sign in to comment.