|
12 | 12 | from functools import partial
|
13 | 13 | from importlib import import_module
|
14 | 14 | from pathlib import Path
|
| 15 | +from typing import Optional, Union |
15 | 16 |
|
16 | 17 | from .environment import Environment
|
17 | 18 | from .filelock import FileLock
|
@@ -87,24 +88,149 @@ def import_string(import_name):
|
87 | 88 | raise ImportError(e)
|
88 | 89 |
|
89 | 90 |
|
90 |
| -def cache_path(archive, root_dir, build_id): |
91 |
| - """Returns a ~/.shiv cache directory for unzipping site-packages during bootstrap. |
| 91 | +def is_dir_writeable(path: Path) -> bool: |
| 92 | + """Whether the given Path `path` is writeable or createable. |
92 | 93 |
|
93 |
| - :param ZipFile archive: The zipfile object we are bootstrapping from. |
| 94 | + Returns whether the *extant portion* of the given path is writeable. |
| 95 | + If so, the path is either extant and writeable or its nearest extant |
| 96 | + parent is writeable (and as such the path may be created in a |
| 97 | + writeable form). |
| 98 | +
|
| 99 | + """ |
| 100 | + while not path.exists(): |
| 101 | + parent = path.parent |
| 102 | + |
| 103 | + # reliably determine whether this is the root |
| 104 | + if parent == path: |
| 105 | + break |
| 106 | + |
| 107 | + path = parent |
| 108 | + |
| 109 | + return os.access(path, os.W_OK) |
| 110 | + |
| 111 | + |
| 112 | +# |
| 113 | +# support for py38 |
| 114 | +# |
| 115 | +def is_relative_to(path: Path, root: Union[Path, str]) -> bool: |
| 116 | + """Return True if the path is relative to another path or False.""" |
| 117 | + try: |
| 118 | + path.relative_to(root) |
| 119 | + except ValueError: |
| 120 | + return False |
| 121 | + else: |
| 122 | + return True |
| 123 | + |
| 124 | + |
| 125 | +def is_system_path(path: Path) -> Optional[bool]: |
| 126 | + """Whether the given `path` appears to be a non-user path. |
| 127 | +
|
| 128 | + Returns bool – or None if called on an unsupported platform |
| 129 | + (_i.e._ implicitly False). |
| 130 | +
|
| 131 | + """ |
| 132 | + if sys.platform == 'linux': |
| 133 | + return not is_relative_to(path, '/home') and not is_relative_to(path, '/root') |
| 134 | + |
| 135 | + if sys.platform == 'darwin': |
| 136 | + return not is_relative_to(path, '/Users') |
| 137 | + |
| 138 | + return None |
| 139 | + |
| 140 | + |
| 141 | +def system_root() -> Optional[Path]: |
| 142 | + """The platform-preferred global/system-wide path for cached files. |
| 143 | +
|
| 144 | + Returns Path – or None if called on an unsupported platform. |
| 145 | +
|
| 146 | + """ |
| 147 | + if sys.platform == 'linux': |
| 148 | + return Path('/var/cache') |
| 149 | + |
| 150 | + if sys.platform == 'darwin': |
| 151 | + return Path('/Library/Caches') |
| 152 | + |
| 153 | + return None |
| 154 | + |
| 155 | + |
| 156 | +def user_root() -> Optional[Path]: |
| 157 | + """The platform-preferred user-specific path for cached files. |
| 158 | +
|
| 159 | + Returns Path – or None if called on an unsupported platform. |
| 160 | +
|
| 161 | + """ |
| 162 | + if sys.platform == 'win32': |
| 163 | + root = os.environ.get('LOCALAPPDATA', '').strip() or '~/AppData/Local' |
| 164 | + elif sys.platform == 'linux': |
| 165 | + root = os.environ.get('XDG_CACHE_HOME', '').strip() or '~/.cache' |
| 166 | + elif sys.platform == 'darwin': |
| 167 | + root = '~/Library/Caches' |
| 168 | + else: |
| 169 | + root = None |
| 170 | + |
| 171 | + return Path(root).expanduser() if root is not None else None |
| 172 | + |
| 173 | + |
| 174 | +def platform_cache(archive_path: Path, build_id: str) -> Optional[Path]: |
| 175 | + """Return a platform-compatible default extraction path. |
| 176 | +
|
| 177 | + * If the archive is installed to a system path and a system-wide |
| 178 | + cache is either already populated or writeable by the current user: |
| 179 | + then the system cache will be used. |
| 180 | +
|
| 181 | + * Otherwise: an appropriate user cache will be used, if any. |
| 182 | +
|
| 183 | + """ |
| 184 | + # |
| 185 | + # 1) let's see about a system_root |
| 186 | + # |
| 187 | + if is_system_path(archive_path): |
| 188 | + system_base = system_root() |
| 189 | + |
| 190 | + if system_base is not None: |
| 191 | + root = system_base / archive_path.name |
| 192 | + |
| 193 | + cache = cache_path(archive_path, str(root), False, build_id) |
| 194 | + site_packages = cache / 'site-packages' |
| 195 | + |
| 196 | + if site_packages.exists() or is_dir_writeable(cache): |
| 197 | + return root |
| 198 | + |
| 199 | + # |
| 200 | + # 2) let's try a user path |
| 201 | + # |
| 202 | + user_base = user_root() |
| 203 | + |
| 204 | + if user_base is not None: |
| 205 | + return user_base / archive_path.name |
| 206 | + |
| 207 | + return None |
| 208 | + |
| 209 | + |
| 210 | +def cache_path(archive_path, root_dir, platform_compat, build_id): |
| 211 | + """Returns a shiv cache directory for unzipping site-packages during bootstrap. |
| 212 | +
|
| 213 | + :param Path archive_path: The Path of the archive we are bootstrapping from. |
94 | 214 | :param str root_dir: Optional, either a path or environment variable pointing to a SHIV_ROOT.
|
| 215 | + :param bool platform_compat: Whether to attempt to fall back to a platform-conventional root. |
95 | 216 | :param str build_id: The build id generated at zip creation.
|
96 |
| - """ |
97 | 217 |
|
| 218 | + """ |
98 | 219 | if root_dir:
|
99 |
| - |
100 | 220 | if root_dir.startswith("$"):
|
101 | 221 | root_dir = os.environ.get(root_dir[1:], root_dir[1:])
|
102 | 222 |
|
103 |
| - root_dir = Path(root_dir).expanduser() |
| 223 | + root = Path(root_dir).expanduser() |
| 224 | + elif platform_compat: |
| 225 | + root = platform_cache(archive_path, build_id) |
| 226 | + else: |
| 227 | + root = None |
| 228 | + |
| 229 | + # platform_compat may be False *OR* platform_cache may return None |
| 230 | + if root is None: |
| 231 | + root = Path.home() / ".shiv" |
104 | 232 |
|
105 |
| - root = root_dir or Path("~/.shiv").expanduser() |
106 |
| - name = Path(archive.filename).resolve().name |
107 |
| - return root / f"{name}_{build_id}" |
| 233 | + return root / f"{archive_path.name}_{build_id}" |
108 | 234 |
|
109 | 235 |
|
110 | 236 | def extract_site_packages(archive, target_path, compile_pyc=False, compile_workers=0, force=False):
|
@@ -190,7 +316,8 @@ def bootstrap(): # pragma: no cover
|
190 | 316 | env = Environment.from_json(archive.read("environment.json").decode())
|
191 | 317 |
|
192 | 318 | # get a site-packages directory (from env var or via build id)
|
193 |
| - site_packages = cache_path(archive, env.root, env.build_id) / "site-packages" |
| 319 | + archive_path = Path(archive.filename).resolve() |
| 320 | + site_packages = cache_path(archive_path, env.root, env.platform_root, env.build_id) / "site-packages" |
194 | 321 |
|
195 | 322 | # determine if first run or forcing extract
|
196 | 323 | if not site_packages.exists() or env.force_extract:
|
|
0 commit comments