pbs-installer 2024.3.22__py3-none-any.whl → 2024.3.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pbs_installer/__main__.py +12 -2
- pbs_installer/_install.py +52 -49
- pbs_installer/_utils.py +68 -13
- pbs_installer/_versions.py +1443 -1431
- {pbs_installer-2024.3.22.dist-info → pbs_installer-2024.3.23.dist-info}/METADATA +1 -1
- pbs_installer-2024.3.23.dist-info/RECORD +10 -0
- pbs_installer-2024.3.22.dist-info/RECORD +0 -10
- {pbs_installer-2024.3.22.dist-info → pbs_installer-2024.3.23.dist-info}/WHEEL +0 -0
- {pbs_installer-2024.3.22.dist-info → pbs_installer-2024.3.23.dist-info}/entry_points.txt +0 -0
- {pbs_installer-2024.3.22.dist-info → pbs_installer-2024.3.23.dist-info}/licenses/LICENSE +0 -0
pbs_installer/__main__.py
CHANGED
|
@@ -50,10 +50,15 @@ def main():
|
|
|
50
50
|
archs, platforms = get_available_arch_platforms()
|
|
51
51
|
parser = ArgumentParser("pbs-install", description="Installer for Python Build Standalone")
|
|
52
52
|
install_group = parser.add_argument_group("Install Arguments")
|
|
53
|
-
install_group.add_argument(
|
|
53
|
+
install_group.add_argument(
|
|
54
|
+
"version", help="The version of Python to install, e.g. 3.8, 3.10.4, pypy@3.10"
|
|
55
|
+
)
|
|
54
56
|
install_group.add_argument(
|
|
55
57
|
"--version-dir", help="Install to a subdirectory named by the version", action="store_true"
|
|
56
58
|
)
|
|
59
|
+
install_group.add_argument(
|
|
60
|
+
"--build-dir", help="Include the build directory", action="store_true"
|
|
61
|
+
)
|
|
57
62
|
install_group.add_argument(
|
|
58
63
|
"-d", "--destination", help="The directory to install to", required=True
|
|
59
64
|
)
|
|
@@ -66,12 +71,17 @@ def main():
|
|
|
66
71
|
|
|
67
72
|
args = parser.parse_args()
|
|
68
73
|
_setup_logger(args.verbose)
|
|
74
|
+
impl, has_amp, version = args.version.rpartition("@")
|
|
75
|
+
if not has_amp:
|
|
76
|
+
impl = "cpython"
|
|
69
77
|
install(
|
|
70
|
-
|
|
78
|
+
version,
|
|
71
79
|
args.destination,
|
|
72
80
|
version_dir=args.version_dir,
|
|
73
81
|
arch=args.arch,
|
|
74
82
|
platform=args.platform,
|
|
83
|
+
implementation=impl,
|
|
84
|
+
build_dir=args.build_dir,
|
|
75
85
|
)
|
|
76
86
|
print("Done!")
|
|
77
87
|
|
pbs_installer/_install.py
CHANGED
|
@@ -4,18 +4,18 @@ import hashlib
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import tempfile
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import TYPE_CHECKING, Tuple
|
|
8
8
|
from urllib.parse import unquote
|
|
9
9
|
|
|
10
|
-
from ._utils import PythonVersion, get_arch_platform
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
10
|
+
from ._utils import PythonVersion, get_arch_platform
|
|
13
11
|
|
|
14
12
|
if TYPE_CHECKING:
|
|
15
13
|
import httpx
|
|
16
14
|
from _typeshed import StrPath
|
|
17
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
18
17
|
THIS_ARCH, THIS_PLATFORM = get_arch_platform()
|
|
18
|
+
PythonFile = Tuple[str, str | None]
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def _get_headers() -> dict[str, str] | None:
|
|
@@ -29,14 +29,18 @@ def _get_headers() -> dict[str, str] | None:
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def get_download_link(
|
|
32
|
-
request: str,
|
|
33
|
-
|
|
32
|
+
request: str,
|
|
33
|
+
arch: str = THIS_ARCH,
|
|
34
|
+
platform: str = THIS_PLATFORM,
|
|
35
|
+
implementation: str = "cpython",
|
|
36
|
+
) -> tuple[PythonVersion, PythonFile]:
|
|
34
37
|
"""Get the download URL matching the given requested version.
|
|
35
38
|
|
|
36
39
|
Parameters:
|
|
37
40
|
request: The version of Python to install, e.g. 3.8,3.10.4
|
|
38
41
|
arch: The architecture to install, e.g. x86_64, arm64
|
|
39
42
|
platform: The platform to install, e.g. linux, macos
|
|
43
|
+
implementation: The implementation of Python to install, allowed values are 'cpython' and 'pypy'
|
|
40
44
|
|
|
41
45
|
Returns:
|
|
42
46
|
A tuple of the PythonVersion and the download URL
|
|
@@ -48,47 +52,35 @@ def get_download_link(
|
|
|
48
52
|
"""
|
|
49
53
|
from ._versions import PYTHON_VERSIONS
|
|
50
54
|
|
|
51
|
-
if arch is None:
|
|
52
|
-
arch = THIS_ARCH
|
|
53
|
-
if platform is None:
|
|
54
|
-
platform = THIS_PLATFORM
|
|
55
|
-
|
|
56
55
|
for py_ver, urls in PYTHON_VERSIONS.items():
|
|
57
|
-
if not py_ver.matches(request):
|
|
56
|
+
if not py_ver.matches(request, implementation):
|
|
58
57
|
continue
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
break
|
|
67
|
-
raise ValueError(f"Could not find a CPython {request!r} matching this system")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _read_sha256(url: str, client: httpx.Client) -> str | None:
|
|
71
|
-
resp = client.get(url + ".sha256", headers=_get_headers())
|
|
72
|
-
if not resp.is_success:
|
|
73
|
-
logger.warning("No checksum found for %s, this would be insecure", url)
|
|
74
|
-
return None
|
|
75
|
-
return resp.text.strip()
|
|
59
|
+
matched = urls.get((platform, arch))
|
|
60
|
+
if matched is not None:
|
|
61
|
+
return py_ver, matched
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Could not find a version matching version={request!r}, implementation={implementation}"
|
|
64
|
+
)
|
|
76
65
|
|
|
77
66
|
|
|
78
|
-
def download(
|
|
67
|
+
def download(
|
|
68
|
+
python_file: PythonFile, destination: StrPath, client: httpx.Client | None = None
|
|
69
|
+
) -> str:
|
|
79
70
|
"""Download the given url to the destination.
|
|
80
71
|
|
|
81
72
|
Note: Extras required
|
|
82
73
|
`pbs-installer[download]` must be installed to use this function.
|
|
83
74
|
|
|
84
75
|
Parameters:
|
|
85
|
-
|
|
76
|
+
python_file: The (url, checksum) tuple to download
|
|
86
77
|
destination: The file path to download to
|
|
87
78
|
client: A http.Client to use for downloading, or None to create a new one
|
|
88
79
|
|
|
89
80
|
Returns:
|
|
90
81
|
The original filename of the downloaded file
|
|
91
82
|
"""
|
|
83
|
+
url, checksum = python_file
|
|
92
84
|
logger.debug("Downloading url %s to %s", url, destination)
|
|
93
85
|
try:
|
|
94
86
|
import httpx
|
|
@@ -100,9 +92,13 @@ def download(url: str, destination: StrPath, client: httpx.Client | None = None)
|
|
|
100
92
|
|
|
101
93
|
filename = unquote(url.rsplit("/")[-1])
|
|
102
94
|
hasher = hashlib.sha256()
|
|
103
|
-
|
|
95
|
+
if not checksum:
|
|
96
|
+
logger.warning("No checksum found for %s, this would be insecure", url)
|
|
104
97
|
|
|
105
|
-
with
|
|
98
|
+
with (
|
|
99
|
+
open(destination, "wb") as f,
|
|
100
|
+
client.stream("GET", url, headers=_get_headers()) as resp,
|
|
101
|
+
):
|
|
106
102
|
resp.raise_for_status()
|
|
107
103
|
for chunk in resp.iter_bytes(chunk_size=8192):
|
|
108
104
|
if checksum:
|
|
@@ -115,7 +111,10 @@ def download(url: str, destination: StrPath, client: httpx.Client | None = None)
|
|
|
115
111
|
|
|
116
112
|
|
|
117
113
|
def install_file(
|
|
118
|
-
filename: StrPath,
|
|
114
|
+
filename: StrPath,
|
|
115
|
+
destination: StrPath,
|
|
116
|
+
original_filename: str | None = None,
|
|
117
|
+
build_dir: bool = False,
|
|
119
118
|
) -> None:
|
|
120
119
|
"""Unpack the downloaded file to the destination.
|
|
121
120
|
|
|
@@ -126,11 +125,10 @@ def install_file(
|
|
|
126
125
|
filename: The file to unpack
|
|
127
126
|
destination: The directory to unpack to
|
|
128
127
|
original_filename: The original filename of the file, if it was renamed
|
|
128
|
+
build_dir: Whether to include the `build/` directory from indygreg builds
|
|
129
129
|
"""
|
|
130
130
|
|
|
131
|
-
import
|
|
132
|
-
|
|
133
|
-
import zstandard as zstd
|
|
131
|
+
from ._utils import unpack_tar, unpack_zip, unpack_zst
|
|
134
132
|
|
|
135
133
|
if original_filename is None:
|
|
136
134
|
original_filename = str(filename)
|
|
@@ -141,17 +139,11 @@ def install_file(
|
|
|
141
139
|
original_filename,
|
|
142
140
|
)
|
|
143
141
|
if original_filename.endswith(".zst"):
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
dctx.copy_stream(ifh, ofh)
|
|
148
|
-
ofh.seek(0)
|
|
149
|
-
with tarfile.open(fileobj=ofh) as z:
|
|
150
|
-
unpack_tar(z, destination, 1)
|
|
151
|
-
|
|
142
|
+
unpack_zst(filename, destination, build_dir)
|
|
143
|
+
elif original_filename.endswith(".zip"):
|
|
144
|
+
unpack_zip(filename, destination, build_dir)
|
|
152
145
|
else:
|
|
153
|
-
|
|
154
|
-
unpack_tar(z, destination, 1)
|
|
146
|
+
unpack_tar(filename, destination, build_dir)
|
|
155
147
|
|
|
156
148
|
|
|
157
149
|
def install(
|
|
@@ -161,6 +153,8 @@ def install(
|
|
|
161
153
|
client: httpx.Client | None = None,
|
|
162
154
|
arch: str | None = None,
|
|
163
155
|
platform: str | None = None,
|
|
156
|
+
implementation: str = "cpython",
|
|
157
|
+
build_dir: bool = False,
|
|
164
158
|
) -> None:
|
|
165
159
|
"""Download and install the requested python version.
|
|
166
160
|
|
|
@@ -174,6 +168,8 @@ def install(
|
|
|
174
168
|
client: A httpx.Client to use for downloading
|
|
175
169
|
arch: The architecture to install, e.g. x86_64, arm64
|
|
176
170
|
platform: The platform to install, e.g. linux, macos
|
|
171
|
+
implementation: The implementation of Python to install, allowed values are 'cpython' and 'pypy'
|
|
172
|
+
build_dir: Whether to include the `build/` directory from indygreg builds
|
|
177
173
|
|
|
178
174
|
Examples:
|
|
179
175
|
>>> install("3.10", "./python")
|
|
@@ -181,12 +177,19 @@ def install(
|
|
|
181
177
|
>>> install("3.10", "./python", version_dir=True)
|
|
182
178
|
Installing cpython@3.10.4 to ./python/cpython@3.10.4
|
|
183
179
|
"""
|
|
184
|
-
|
|
180
|
+
if platform is None:
|
|
181
|
+
platform = THIS_PLATFORM
|
|
182
|
+
if arch is None:
|
|
183
|
+
arch = THIS_ARCH
|
|
184
|
+
|
|
185
|
+
ver, python_file = get_download_link(
|
|
186
|
+
request, arch=arch, platform=platform, implementation=implementation
|
|
187
|
+
)
|
|
185
188
|
if version_dir:
|
|
186
189
|
destination = os.path.join(destination, str(ver))
|
|
187
190
|
logger.debug("Installing %s to %s", ver, destination)
|
|
188
191
|
os.makedirs(destination, exist_ok=True)
|
|
189
192
|
with tempfile.NamedTemporaryFile() as tf:
|
|
190
193
|
tf.close()
|
|
191
|
-
original_filename = download(
|
|
192
|
-
install_file(tf.name, destination, original_filename)
|
|
194
|
+
original_filename = download(python_file, tf.name, client)
|
|
195
|
+
install_file(tf.name, destination, original_filename, build_dir)
|
pbs_installer/_utils.py
CHANGED
|
@@ -7,21 +7,25 @@ if TYPE_CHECKING:
|
|
|
7
7
|
from _typeshed import StrPath
|
|
8
8
|
|
|
9
9
|
ARCH_MAPPING = {
|
|
10
|
-
"
|
|
10
|
+
"arm64": "aarch64",
|
|
11
11
|
"amd64": "x86_64",
|
|
12
|
+
"i686": "x86",
|
|
12
13
|
}
|
|
14
|
+
PLATFORM_MAPPING = {"darwin": "macos"}
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class PythonVersion(NamedTuple):
|
|
16
|
-
|
|
18
|
+
implementation: str
|
|
17
19
|
major: int
|
|
18
20
|
minor: int
|
|
19
21
|
micro: int
|
|
20
22
|
|
|
21
23
|
def __str__(self) -> str:
|
|
22
|
-
return f"{self.
|
|
24
|
+
return f"{self.implementation}@{self.major}.{self.minor}.{self.micro}"
|
|
23
25
|
|
|
24
|
-
def matches(self, request: str) -> bool:
|
|
26
|
+
def matches(self, request: str, implementation: str) -> bool:
|
|
27
|
+
if implementation != self.implementation:
|
|
28
|
+
return False
|
|
25
29
|
try:
|
|
26
30
|
parts = tuple(int(v) for v in request.split("."))
|
|
27
31
|
except ValueError:
|
|
@@ -46,17 +50,68 @@ def get_arch_platform() -> tuple[str, str]:
|
|
|
46
50
|
|
|
47
51
|
plat = platform.system().lower()
|
|
48
52
|
arch = platform.machine().lower()
|
|
49
|
-
return ARCH_MAPPING.get(arch, arch), plat
|
|
53
|
+
return ARCH_MAPPING.get(arch, arch), PLATFORM_MAPPING.get(plat, plat)
|
|
50
54
|
|
|
51
55
|
|
|
52
|
-
def
|
|
56
|
+
def _unpack_tar(tf: tarfile.TarFile, destination: StrPath, build_dir: bool = False) -> None:
|
|
53
57
|
"""Unpack the tarfile to the destination, with the first skip_parts parts of the path removed"""
|
|
58
|
+
members: list[tarfile.TarInfo] = []
|
|
59
|
+
has_build = any(
|
|
60
|
+
(p := fn.lstrip("/").split("/")) and len(p) > 1 and p[1] == "build" for fn in tf.getnames()
|
|
61
|
+
)
|
|
54
62
|
for member in tf.getmembers():
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
parts = member.name.lstrip("/").split("/")
|
|
64
|
+
if build_dir or not has_build:
|
|
65
|
+
member.name = "/".join(parts[1:])
|
|
66
|
+
elif len(parts) > 1 and parts[1] == "install":
|
|
67
|
+
member.name = "/".join(parts[2:])
|
|
68
|
+
else:
|
|
69
|
+
continue
|
|
70
|
+
if member.name:
|
|
71
|
+
members.append(member)
|
|
72
|
+
tf.extractall(destination, members=members)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def unpack_tar(filename: str, destination: StrPath, build_dir: bool = False) -> None:
|
|
76
|
+
"""Unpack the tarfile to the destination"""
|
|
77
|
+
with tarfile.open(filename) as z:
|
|
78
|
+
_unpack_tar(z, destination, build_dir=build_dir)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def unpack_zst(filename: str, destination: StrPath, build_dir: bool = False) -> None:
|
|
82
|
+
"""Unpack the zstd compressed tarfile to the destination"""
|
|
83
|
+
import tempfile
|
|
84
|
+
|
|
85
|
+
import zstandard as zstd
|
|
86
|
+
|
|
87
|
+
dctx = zstd.ZstdDecompressor()
|
|
88
|
+
with tempfile.TemporaryFile(suffix=".tar") as ofh:
|
|
89
|
+
with open(filename, "rb") as ifh:
|
|
90
|
+
dctx.copy_stream(ifh, ofh)
|
|
91
|
+
ofh.seek(0)
|
|
92
|
+
with tarfile.open(fileobj=ofh) as z:
|
|
93
|
+
_unpack_tar(z, destination, build_dir=build_dir)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def unpack_zip(filename: str, destination: StrPath, build_dir: bool = False) -> None:
|
|
97
|
+
"""Unpack the zip file to the destination"""
|
|
98
|
+
import zipfile
|
|
99
|
+
|
|
100
|
+
with zipfile.ZipFile(filename) as z:
|
|
101
|
+
members: list[zipfile.ZipInfo] = []
|
|
102
|
+
has_build = any(fn.lstrip("/").split("/")[1] == "build" for fn in z.namelist())
|
|
103
|
+
for member in z.infolist():
|
|
104
|
+
parts = member.filename.lstrip("/").split("/")
|
|
105
|
+
if (build_dir or not has_build) and len(parts) > 1:
|
|
106
|
+
member.filename = "/".join(parts[1:])
|
|
107
|
+
elif len(parts) > 1 and parts[1] == "install":
|
|
108
|
+
member.filename = "/".join(parts[2:])
|
|
109
|
+
else:
|
|
110
|
+
continue
|
|
111
|
+
if member.filename:
|
|
112
|
+
members.append(member)
|
|
113
|
+
|
|
114
|
+
z.extractall(destination, members=members)
|
|
60
115
|
|
|
61
116
|
|
|
62
117
|
def get_available_arch_platforms() -> tuple[list[str], list[str]]:
|
|
@@ -66,6 +121,6 @@ def get_available_arch_platforms() -> tuple[list[str], list[str]]:
|
|
|
66
121
|
platforms: set[str] = set()
|
|
67
122
|
for items in PYTHON_VERSIONS.values():
|
|
68
123
|
for item in items:
|
|
69
|
-
|
|
70
|
-
|
|
124
|
+
platforms.add(item[0])
|
|
125
|
+
archs.add(item[1])
|
|
71
126
|
return sorted(archs), sorted(platforms)
|