pbs-installer 2024.2.26__tar.gz → 2024.3.23__tar.gz
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-2024.2.26 → pbs_installer-2024.3.23}/PKG-INFO +5 -3
- {pbs_installer-2024.2.26 → pbs_installer-2024.3.23}/README.md +2 -0
- {pbs_installer-2024.2.26 → pbs_installer-2024.3.23}/pyproject.toml +12 -14
- {pbs_installer-2024.2.26 → pbs_installer-2024.3.23}/src/pbs_installer/__init__.py +1 -4
- {pbs_installer-2024.2.26 → pbs_installer-2024.3.23}/src/pbs_installer/__main__.py +12 -2
- pbs_installer-2024.3.23/src/pbs_installer/_install.py +195 -0
- pbs_installer-2024.3.23/src/pbs_installer/_utils.py +126 -0
- {pbs_installer-2024.2.26 → pbs_installer-2024.3.23}/src/pbs_installer/_versions.py +1443 -1431
- pbs_installer-2024.2.26/src/pbs_installer/_install.py +0 -165
- pbs_installer-2024.2.26/src/pbs_installer/_utils.py +0 -71
- {pbs_installer-2024.2.26 → pbs_installer-2024.3.23}/LICENSE +0 -0
- {pbs_installer-2024.2.26 → pbs_installer-2024.3.23}/tests/__init__.py +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pbs-installer
|
|
3
|
-
Version: 2024.
|
|
3
|
+
Version: 2024.3.23
|
|
4
4
|
Summary: Installer for Python Build Standalone
|
|
5
5
|
Author-Email: Frost Ming <me@frostming.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
8
|
Provides-Extra: download
|
|
9
9
|
Provides-Extra: install
|
|
10
10
|
Provides-Extra: all
|
|
11
|
-
Requires-Dist:
|
|
11
|
+
Requires-Dist: httpx<1,>=0.27.0; extra == "download"
|
|
12
12
|
Requires-Dist: zstandard>=0.21.0; extra == "install"
|
|
13
13
|
Requires-Dist: pbs-installer[download,install]; extra == "all"
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
@@ -20,3 +20,5 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
An installer for @indygreg's [python-build-standalone](https://github.com/indygreg/python-build-standalone)
|
|
21
21
|
|
|
22
22
|
The list of python versions are kept sync with the upstream automatically, via a periodically GitHub Action.
|
|
23
|
+
|
|
24
|
+
[📖 Read the docs](http://pbs-installer.readthedocs.io/)
|
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
An installer for @indygreg's [python-build-standalone](https://github.com/indygreg/python-build-standalone)
|
|
6
6
|
|
|
7
7
|
The list of python versions are kept sync with the upstream automatically, via a periodically GitHub Action.
|
|
8
|
+
|
|
9
|
+
[📖 Read the docs](http://pbs-installer.readthedocs.io/)
|
|
@@ -4,18 +4,17 @@ description = "Installer for Python Build Standalone"
|
|
|
4
4
|
authors = [
|
|
5
5
|
{ name = "Frost Ming", email = "me@frostming.com" },
|
|
6
6
|
]
|
|
7
|
-
|
|
8
|
-
requires-python = ">=3.7"
|
|
7
|
+
requires-python = ">=3.8"
|
|
9
8
|
readme = "README.md"
|
|
10
9
|
dynamic = []
|
|
11
|
-
version = "2024.
|
|
10
|
+
version = "2024.3.23"
|
|
12
11
|
|
|
13
12
|
[project.license]
|
|
14
13
|
text = "MIT"
|
|
15
14
|
|
|
16
15
|
[project.optional-dependencies]
|
|
17
16
|
download = [
|
|
18
|
-
"
|
|
17
|
+
"httpx<1,>=0.27.0",
|
|
19
18
|
]
|
|
20
19
|
install = [
|
|
21
20
|
"zstandard>=0.21.0",
|
|
@@ -33,25 +32,24 @@ requires = [
|
|
|
33
32
|
]
|
|
34
33
|
build-backend = "pdm.backend"
|
|
35
34
|
|
|
36
|
-
[tool.pdm.dev-dependencies]
|
|
37
|
-
dev = [
|
|
38
|
-
"black>=23.3.0",
|
|
39
|
-
]
|
|
40
|
-
|
|
41
35
|
[tool.pdm.version]
|
|
42
36
|
source = "scm"
|
|
43
37
|
|
|
44
38
|
[tool.pdm.scripts.update]
|
|
45
39
|
shell = "./scripts/update.sh"
|
|
46
40
|
|
|
47
|
-
[tool.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
[tool.pdm.dev-dependencies]
|
|
42
|
+
doc = [
|
|
43
|
+
"mkdocs>=1.5.3",
|
|
44
|
+
"mkdocs-material>=9.5.14",
|
|
45
|
+
"mkdocstrings[python]>=0.24",
|
|
46
|
+
]
|
|
51
47
|
|
|
52
48
|
[tool.ruff]
|
|
53
49
|
line-length = 100
|
|
50
|
+
target-version = "py310"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
54
53
|
extend-select = [
|
|
55
54
|
"I",
|
|
56
55
|
]
|
|
57
|
-
target-version = "py310"
|
|
@@ -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
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from typing import TYPE_CHECKING, Tuple
|
|
8
|
+
from urllib.parse import unquote
|
|
9
|
+
|
|
10
|
+
from ._utils import PythonVersion, get_arch_platform
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import httpx
|
|
14
|
+
from _typeshed import StrPath
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
THIS_ARCH, THIS_PLATFORM = get_arch_platform()
|
|
18
|
+
PythonFile = Tuple[str, str | None]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_headers() -> dict[str, str] | None:
|
|
22
|
+
TOKEN = os.getenv("GITHUB_TOKEN")
|
|
23
|
+
if TOKEN is None:
|
|
24
|
+
return None
|
|
25
|
+
return {
|
|
26
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
27
|
+
"Authorization": f"Bearer {TOKEN}",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_download_link(
|
|
32
|
+
request: str,
|
|
33
|
+
arch: str = THIS_ARCH,
|
|
34
|
+
platform: str = THIS_PLATFORM,
|
|
35
|
+
implementation: str = "cpython",
|
|
36
|
+
) -> tuple[PythonVersion, PythonFile]:
|
|
37
|
+
"""Get the download URL matching the given requested version.
|
|
38
|
+
|
|
39
|
+
Parameters:
|
|
40
|
+
request: The version of Python to install, e.g. 3.8,3.10.4
|
|
41
|
+
arch: The architecture to install, e.g. x86_64, arm64
|
|
42
|
+
platform: The platform to install, e.g. linux, macos
|
|
43
|
+
implementation: The implementation of Python to install, allowed values are 'cpython' and 'pypy'
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A tuple of the PythonVersion and the download URL
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
>>> get_download_link("3.10", "x86_64", "linux")
|
|
50
|
+
(PythonVersion(kind='cpython', major=3, minor=10, micro=13),
|
|
51
|
+
'https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-unknown-linux-gnu-pgo%2Blto-full.tar.zst')
|
|
52
|
+
"""
|
|
53
|
+
from ._versions import PYTHON_VERSIONS
|
|
54
|
+
|
|
55
|
+
for py_ver, urls in PYTHON_VERSIONS.items():
|
|
56
|
+
if not py_ver.matches(request, implementation):
|
|
57
|
+
continue
|
|
58
|
+
|
|
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
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def download(
|
|
68
|
+
python_file: PythonFile, destination: StrPath, client: httpx.Client | None = None
|
|
69
|
+
) -> str:
|
|
70
|
+
"""Download the given url to the destination.
|
|
71
|
+
|
|
72
|
+
Note: Extras required
|
|
73
|
+
`pbs-installer[download]` must be installed to use this function.
|
|
74
|
+
|
|
75
|
+
Parameters:
|
|
76
|
+
python_file: The (url, checksum) tuple to download
|
|
77
|
+
destination: The file path to download to
|
|
78
|
+
client: A http.Client to use for downloading, or None to create a new one
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The original filename of the downloaded file
|
|
82
|
+
"""
|
|
83
|
+
url, checksum = python_file
|
|
84
|
+
logger.debug("Downloading url %s to %s", url, destination)
|
|
85
|
+
try:
|
|
86
|
+
import httpx
|
|
87
|
+
except ModuleNotFoundError:
|
|
88
|
+
raise RuntimeError("You must install httpx to use this function") from None
|
|
89
|
+
|
|
90
|
+
if client is None:
|
|
91
|
+
client = httpx.Client(trust_env=True, follow_redirects=True)
|
|
92
|
+
|
|
93
|
+
filename = unquote(url.rsplit("/")[-1])
|
|
94
|
+
hasher = hashlib.sha256()
|
|
95
|
+
if not checksum:
|
|
96
|
+
logger.warning("No checksum found for %s, this would be insecure", url)
|
|
97
|
+
|
|
98
|
+
with (
|
|
99
|
+
open(destination, "wb") as f,
|
|
100
|
+
client.stream("GET", url, headers=_get_headers()) as resp,
|
|
101
|
+
):
|
|
102
|
+
resp.raise_for_status()
|
|
103
|
+
for chunk in resp.iter_bytes(chunk_size=8192):
|
|
104
|
+
if checksum:
|
|
105
|
+
hasher.update(chunk)
|
|
106
|
+
f.write(chunk)
|
|
107
|
+
|
|
108
|
+
if checksum and hasher.hexdigest() != checksum:
|
|
109
|
+
raise RuntimeError(f"Checksum mismatch. Expected {checksum}, got {hasher.hexdigest()}")
|
|
110
|
+
return filename
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def install_file(
|
|
114
|
+
filename: StrPath,
|
|
115
|
+
destination: StrPath,
|
|
116
|
+
original_filename: str | None = None,
|
|
117
|
+
build_dir: bool = False,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Unpack the downloaded file to the destination.
|
|
120
|
+
|
|
121
|
+
Note: Extras required
|
|
122
|
+
`pbs-installer[install]` must be installed to use this function.
|
|
123
|
+
|
|
124
|
+
Parameters:
|
|
125
|
+
filename: The file to unpack
|
|
126
|
+
destination: The directory to unpack to
|
|
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
|
+
"""
|
|
130
|
+
|
|
131
|
+
from ._utils import unpack_tar, unpack_zip, unpack_zst
|
|
132
|
+
|
|
133
|
+
if original_filename is None:
|
|
134
|
+
original_filename = str(filename)
|
|
135
|
+
logger.debug(
|
|
136
|
+
"Extracting file %s to %s with original filename %s",
|
|
137
|
+
filename,
|
|
138
|
+
destination,
|
|
139
|
+
original_filename,
|
|
140
|
+
)
|
|
141
|
+
if original_filename.endswith(".zst"):
|
|
142
|
+
unpack_zst(filename, destination, build_dir)
|
|
143
|
+
elif original_filename.endswith(".zip"):
|
|
144
|
+
unpack_zip(filename, destination, build_dir)
|
|
145
|
+
else:
|
|
146
|
+
unpack_tar(filename, destination, build_dir)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def install(
|
|
150
|
+
request: str,
|
|
151
|
+
destination: StrPath,
|
|
152
|
+
version_dir: bool = False,
|
|
153
|
+
client: httpx.Client | None = None,
|
|
154
|
+
arch: str | None = None,
|
|
155
|
+
platform: str | None = None,
|
|
156
|
+
implementation: str = "cpython",
|
|
157
|
+
build_dir: bool = False,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Download and install the requested python version.
|
|
160
|
+
|
|
161
|
+
Note: Extras required
|
|
162
|
+
`pbs-installer[all]` must be installed to use this function.
|
|
163
|
+
|
|
164
|
+
Parameters:
|
|
165
|
+
request: The version of Python to install, e.g. 3.8,3.10.4
|
|
166
|
+
destination: The directory to install to
|
|
167
|
+
version_dir: Whether to install to a subdirectory named with the python version
|
|
168
|
+
client: A httpx.Client to use for downloading
|
|
169
|
+
arch: The architecture to install, e.g. x86_64, arm64
|
|
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
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
>>> install("3.10", "./python")
|
|
176
|
+
Installing cpython@3.10.4 to ./python
|
|
177
|
+
>>> install("3.10", "./python", version_dir=True)
|
|
178
|
+
Installing cpython@3.10.4 to ./python/cpython@3.10.4
|
|
179
|
+
"""
|
|
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
|
+
)
|
|
188
|
+
if version_dir:
|
|
189
|
+
destination = os.path.join(destination, str(ver))
|
|
190
|
+
logger.debug("Installing %s to %s", ver, destination)
|
|
191
|
+
os.makedirs(destination, exist_ok=True)
|
|
192
|
+
with tempfile.NamedTemporaryFile() as tf:
|
|
193
|
+
tf.close()
|
|
194
|
+
original_filename = download(python_file, tf.name, client)
|
|
195
|
+
install_file(tf.name, destination, original_filename, build_dir)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tarfile
|
|
4
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from _typeshed import StrPath
|
|
8
|
+
|
|
9
|
+
ARCH_MAPPING = {
|
|
10
|
+
"arm64": "aarch64",
|
|
11
|
+
"amd64": "x86_64",
|
|
12
|
+
"i686": "x86",
|
|
13
|
+
}
|
|
14
|
+
PLATFORM_MAPPING = {"darwin": "macos"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PythonVersion(NamedTuple):
|
|
18
|
+
implementation: str
|
|
19
|
+
major: int
|
|
20
|
+
minor: int
|
|
21
|
+
micro: int
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
return f"{self.implementation}@{self.major}.{self.minor}.{self.micro}"
|
|
25
|
+
|
|
26
|
+
def matches(self, request: str, implementation: str) -> bool:
|
|
27
|
+
if implementation != self.implementation:
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
parts = tuple(int(v) for v in request.split("."))
|
|
31
|
+
except ValueError:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"Invalid version: {request!r}, each part must be an integer"
|
|
34
|
+
) from None
|
|
35
|
+
|
|
36
|
+
if len(parts) < 1:
|
|
37
|
+
raise ValueError("Version must have at least one part")
|
|
38
|
+
|
|
39
|
+
if parts[0] != self.major:
|
|
40
|
+
return False
|
|
41
|
+
if len(parts) > 1 and parts[1] != self.minor:
|
|
42
|
+
return False
|
|
43
|
+
if len(parts) > 2 and parts[2] != self.micro:
|
|
44
|
+
return False
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_arch_platform() -> tuple[str, str]:
|
|
49
|
+
import platform
|
|
50
|
+
|
|
51
|
+
plat = platform.system().lower()
|
|
52
|
+
arch = platform.machine().lower()
|
|
53
|
+
return ARCH_MAPPING.get(arch, arch), PLATFORM_MAPPING.get(plat, plat)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _unpack_tar(tf: tarfile.TarFile, destination: StrPath, build_dir: bool = False) -> None:
|
|
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
|
+
)
|
|
62
|
+
for member in tf.getmembers():
|
|
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)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_available_arch_platforms() -> tuple[list[str], list[str]]:
|
|
118
|
+
from ._versions import PYTHON_VERSIONS
|
|
119
|
+
|
|
120
|
+
archs: set[str] = set()
|
|
121
|
+
platforms: set[str] = set()
|
|
122
|
+
for items in PYTHON_VERSIONS.values():
|
|
123
|
+
for item in items:
|
|
124
|
+
platforms.add(item[0])
|
|
125
|
+
archs.add(item[1])
|
|
126
|
+
return sorted(archs), sorted(platforms)
|