|
1 | 1 | import sys |
2 | 2 | from collections.abc import Sequence |
3 | 3 | from pathlib import Path |
| 4 | +from tempfile import mkdtemp |
4 | 5 |
|
5 | 6 | from packaging.utils import canonicalize_name |
6 | 7 |
|
| 8 | +from pipx.commands.common import add_suffix |
7 | 9 | from pipx.commands.inject import inject_dep |
8 | 10 | from pipx.commands.install import install |
9 | | -from pipx.commands.uninstall import uninstall |
| 11 | +from pipx.commands.uninstall import _get_venv_resource_paths |
10 | 12 | from pipx.constants import ( |
11 | 13 | EXIT_CODE_OK, |
12 | 14 | EXIT_CODE_REINSTALL_INVALID_PYTHON, |
13 | 15 | EXIT_CODE_REINSTALL_VENV_NONEXISTENT, |
| 16 | + MAN_SECTIONS, |
14 | 17 | ExitCode, |
15 | 18 | ) |
16 | | -from pipx.emojis import error, sleep |
17 | | -from pipx.util import PipxError |
| 19 | +from pipx.emojis import error, sleep, stars |
| 20 | +from pipx.util import PipxError, rmdir, safe_unlink |
18 | 21 | from pipx.venv import Venv, VenvContainer |
19 | 22 |
|
20 | 23 |
|
| 24 | +def _create_reinstall_backup(venv_dir: Path) -> Path: |
| 25 | + backup_dir = Path(mkdtemp(prefix=f".{venv_dir.name}-", suffix="-pipx-reinstall", dir=venv_dir.parent)) |
| 26 | + backup_dir.rmdir() |
| 27 | + venv_dir.rename(backup_dir) |
| 28 | + return backup_dir |
| 29 | + |
| 30 | + |
| 31 | +def _restore_reinstall_backup(venv_dir: Path, restore_venv_dir: Path, backup_dir: Path) -> None: |
| 32 | + rmdir(venv_dir) |
| 33 | + backup_dir.rename(restore_venv_dir) |
| 34 | + |
| 35 | + |
| 36 | +def _get_reinstall_resource_paths(venv: Venv, local_bin_dir: Path, local_man_dir: Path) -> set[Path]: |
| 37 | + resource_paths = _get_venv_resource_paths("app", venv, venv.bin_path, local_bin_dir) |
| 38 | + for man_section in MAN_SECTIONS: |
| 39 | + resource_paths |= _get_venv_resource_paths("man", venv, venv.man_path / man_section, local_man_dir / man_section) |
| 40 | + return resource_paths |
| 41 | + |
| 42 | + |
| 43 | +def _get_expected_reinstall_resource_paths(venv: Venv, local_bin_dir: Path, local_man_dir: Path) -> set[Path]: |
| 44 | + resource_paths: set[Path] = set() |
| 45 | + for package_info in venv.package_metadata.values(): |
| 46 | + if package_info.include_apps: |
| 47 | + for app_path in package_info.app_paths: |
| 48 | + resource_paths.add(local_bin_dir / add_suffix(app_path.name, package_info.suffix)) |
| 49 | + for man_path in package_info.man_paths: |
| 50 | + resource_paths.add(local_man_dir / man_path.parent.name / man_path.name) |
| 51 | + if package_info.include_dependencies: |
| 52 | + for app_paths in package_info.app_paths_of_dependencies.values(): |
| 53 | + for app_path in app_paths: |
| 54 | + resource_paths.add(local_bin_dir / add_suffix(app_path.name, package_info.suffix)) |
| 55 | + for man_paths in package_info.man_paths_of_dependencies.values(): |
| 56 | + for man_path in man_paths: |
| 57 | + resource_paths.add(local_man_dir / man_path.parent.name / man_path.name) |
| 58 | + return resource_paths |
| 59 | + |
| 60 | + |
| 61 | +def _remove_stale_reinstall_resources(resource_paths: set[Path]) -> None: |
| 62 | + for path in sorted(resource_paths): |
| 63 | + try: |
| 64 | + safe_unlink(path) |
| 65 | + if path.is_symlink(): |
| 66 | + path.unlink() |
| 67 | + except FileNotFoundError: |
| 68 | + pass |
| 69 | + |
| 70 | + |
21 | 71 | def reinstall( |
22 | 72 | *, |
23 | 73 | venv_dir: Path, |
@@ -59,51 +109,66 @@ def reinstall( |
59 | 109 | if venv.pipx_metadata.main_package.pinned: |
60 | 110 | raise PipxError(f"{error} Package {venv_dir} is pinned. Run `pipx unpin {venv_dir.name}` to unpin it first.") |
61 | 111 |
|
62 | | - uninstall(venv_dir, local_bin_dir, local_man_dir, verbose) |
| 112 | + old_resource_paths = _get_reinstall_resource_paths(venv, local_bin_dir, local_man_dir) |
| 113 | + original_venv_dir = venv_dir |
| 114 | + reinstall_backup_dir = _create_reinstall_backup(venv_dir) |
| 115 | + print(f"uninstalled {venv.name}! {stars}") |
63 | 116 |
|
64 | 117 | # in case legacy original dir name |
65 | 118 | venv_dir = venv_dir.with_name(canonicalize_name(venv_dir.name)) |
66 | 119 |
|
67 | | - # install main package first |
68 | | - install( |
69 | | - venv_dir, |
70 | | - [venv.main_package_name], |
71 | | - [package_or_url], |
72 | | - local_bin_dir, |
73 | | - local_man_dir, |
74 | | - python, |
75 | | - venv.pipx_metadata.main_package.pip_args, |
76 | | - venv.pipx_metadata.venv_args, |
77 | | - verbose, |
78 | | - force=True, |
79 | | - reinstall=True, |
80 | | - include_dependencies=venv.pipx_metadata.main_package.include_dependencies, |
81 | | - preinstall_packages=[], |
82 | | - suffix=venv.pipx_metadata.main_package.suffix, |
83 | | - python_flag_passed=python_flag_passed, |
84 | | - backend=backend or venv.pipx_metadata.backend, |
85 | | - env_backend=env_backend, |
86 | | - ) |
87 | | - |
88 | | - # now install injected packages |
89 | | - for injected_name, injected_package in venv.pipx_metadata.injected_packages.items(): |
90 | | - if injected_package.package_or_url is None: |
91 | | - # This should never happen, but package_or_url is type |
92 | | - # Optional[str] so mypy thinks it could be None |
93 | | - raise PipxError(f"Internal Error injecting package {injected_package} into {venv.name}") |
94 | | - inject_dep( |
| 120 | + try: |
| 121 | + # install main package first |
| 122 | + install( |
95 | 123 | venv_dir, |
96 | | - injected_name, |
97 | | - injected_package.package_or_url, |
98 | | - injected_package.pip_args, |
99 | | - verbose=verbose, |
100 | | - include_apps=injected_package.include_apps, |
101 | | - include_dependencies=injected_package.include_dependencies, |
| 124 | + [venv.main_package_name], |
| 125 | + [package_or_url], |
| 126 | + local_bin_dir, |
| 127 | + local_man_dir, |
| 128 | + python, |
| 129 | + venv.pipx_metadata.main_package.pip_args, |
| 130 | + venv.pipx_metadata.venv_args, |
| 131 | + verbose, |
102 | 132 | force=True, |
| 133 | + reinstall=True, |
| 134 | + include_dependencies=venv.pipx_metadata.main_package.include_dependencies, |
| 135 | + preinstall_packages=[], |
| 136 | + suffix=venv.pipx_metadata.main_package.suffix, |
| 137 | + python_flag_passed=python_flag_passed, |
103 | 138 | backend=backend or venv.pipx_metadata.backend, |
104 | 139 | env_backend=env_backend, |
105 | 140 | ) |
106 | 141 |
|
| 142 | + # now install injected packages |
| 143 | + for injected_name, injected_package in venv.pipx_metadata.injected_packages.items(): |
| 144 | + if injected_package.package_or_url is None: |
| 145 | + # This should never happen, but package_or_url is type |
| 146 | + # Optional[str] so mypy thinks it could be None |
| 147 | + raise PipxError(f"Internal Error injecting package {injected_package} into {venv.name}") |
| 148 | + inject_dep( |
| 149 | + venv_dir, |
| 150 | + injected_name, |
| 151 | + injected_package.package_or_url, |
| 152 | + injected_package.pip_args, |
| 153 | + verbose=verbose, |
| 154 | + include_apps=injected_package.include_apps, |
| 155 | + include_dependencies=injected_package.include_dependencies, |
| 156 | + force=True, |
| 157 | + backend=backend or venv.pipx_metadata.backend, |
| 158 | + env_backend=env_backend, |
| 159 | + ) |
| 160 | + |
| 161 | + new_resource_paths = _get_expected_reinstall_resource_paths( |
| 162 | + Venv(venv_dir, verbose=verbose), local_bin_dir, local_man_dir |
| 163 | + ) |
| 164 | + _remove_stale_reinstall_resources(old_resource_paths - new_resource_paths) |
| 165 | + except (Exception, KeyboardInterrupt): |
| 166 | + _restore_reinstall_backup(venv_dir, original_venv_dir, reinstall_backup_dir) |
| 167 | + print(f"{error} Reinstall failed; restored {venv.name}.", file=sys.stderr) |
| 168 | + raise |
| 169 | + else: |
| 170 | + rmdir(reinstall_backup_dir) |
| 171 | + |
107 | 172 | # Any failure to install will raise PipxError, otherwise success |
108 | 173 | return EXIT_CODE_OK |
109 | 174 |
|
|
0 commit comments