Skip to content

Commit

Permalink
feat: support freethreaded python
Browse files Browse the repository at this point in the history
Signed-off-by: Frost Ming <me@frostming.com>
  • Loading branch information
frostming committed Oct 10, 2024
1 parent 7734012 commit eda2373
Show file tree
Hide file tree
Showing 4 changed files with 978 additions and 924 deletions.
43 changes: 29 additions & 14 deletions scripts/find_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ def __neg__(self) -> Self:
return Version(-self.major, -self.minor, -self.patch)


class VersionKey(NamedTuple):
platform: str
arch: str
install_only: bool
freethreaded: bool

def __repr__(self) -> str:
return repr(tuple(self))


def log(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)

Expand Down Expand Up @@ -98,7 +108,6 @@ class CPythonFinder(Finder):
FLAVOR_PREFERENCES = [
"shared-pgo",
"shared-noopt",
"shared-noopt",
"pgo+lto",
"pgo",
"lto",
Expand Down Expand Up @@ -160,7 +169,7 @@ async def find(self) -> list[PythonDownload]:

async def fetch_indygreg_downloads(self, pages: int = 100) -> list[PythonDownload]:
"""Fetch all the indygreg downloads from the release API."""
results: dict[Version, dict[tuple[str, str, bool], list[PythonDownload]]] = {}
results: dict[Version, dict[VersionKey, list[PythonDownload]]] = {}

for page in range(1, pages):
log(f"Fetching indygreg release page {page}")
Expand All @@ -175,18 +184,23 @@ async def fetch_indygreg_downloads(self, pages: int = 100) -> list[PythonDownloa
download = self.parse_download_url(url)
if download is not None:
install_only = download.triple.flavor == "install_only"
freethreaded = "freethreaded" in download.filename
key = VersionKey(
download.triple.platform,
download.triple.arch,
install_only,
freethreaded,
)
(
results.setdefault(download.version, {})
# For now, we only group by arch and platform, because Rust's PythonVersion doesn't have a notion
# of environment. Flavor will never be used to sort download choices and must not be included in grouping.
.setdefault(
(download.triple.arch, download.triple.platform, install_only), []
)
.setdefault(key, [])
.append(download)
)

downloads = []
for version, platform_downloads in results.items():
for _, platform_downloads in results.items():
for flavors in platform_downloads.values():
best = self.pick_best_download(flavors)
if best is not None:
Expand Down Expand Up @@ -388,22 +402,21 @@ async def fetch_checksums(self, downloads: list[PythonDownload]) -> None:

def build_map(
downloads: list[PythonDownload],
) -> dict[
tuple[PythonImplementation, Version], dict[tuple[str, str, bool], tuple[str, str | None]]
]:
) -> dict[tuple[PythonImplementation, Version], dict[VersionKey, tuple[str, str | None]]]:
versions: dict[
tuple[PythonImplementation, Version], dict[tuple[str, str, bool], tuple[str, str | None]]
tuple[PythonImplementation, Version], dict[VersionKey, tuple[str, str | None]]
] = {}
for download in sorted(downloads, key=lambda d: (d.implementation, -d.version)):
item = versions.setdefault((download.implementation, download.version), {})
platform_triple = (
key = VersionKey(
download.triple.platform,
download.triple.arch,
download.triple.flavor == "install_only",
"freethreaded" in download.filename,
)
if platform_triple in item:
if key in item:
continue
item[platform_triple] = (download.url, download.sha256)
item[key] = (download.url, download.sha256)
return versions


Expand All @@ -416,7 +429,9 @@ def write(line: str) -> None:
write("# @Generated by find_versions.py. DO NOT EDIT.")
write("from __future__ import annotations")
write("from ._utils import PythonVersion")
write("PYTHON_VERSIONS: dict[PythonVersion, dict[tuple[str, str, bool], tuple[str, str | None]]] = {")
write(
"PYTHON_VERSIONS: dict[PythonVersion, dict[tuple[str, str, bool, bool], tuple[str, str | None]]] = {"
)

for key, item in build_map(downloads).items():
write(
Expand Down
26 changes: 20 additions & 6 deletions src/pbs_installer/_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
from ._utils import PythonVersion, get_arch_platform

if TYPE_CHECKING:
from typing import Literal

import httpx
from _typeshed import StrPath

from typing import Literal

PythonImplementation = Literal["cpython", "pypy"]

logger = logging.getLogger(__name__)
Expand All @@ -38,6 +38,7 @@ def get_download_link(
platform: str = THIS_PLATFORM,
implementation: PythonImplementation = "cpython",
build_dir: bool = False,
free_threaded: bool = False,
) -> tuple[PythonVersion, PythonFile]:
"""Get the download URL matching the given requested version.
Expand All @@ -47,6 +48,7 @@ def get_download_link(
platform: The platform to install, e.g. linux, macos
implementation: The implementation of Python to install, allowed values are 'cpython' and 'pypy'
build_dir: Whether to include the `build/` directory from indygreg builds
free_threaded: Whether to install the freethreaded version of Python
Returns:
A tuple of the PythonVersion and the download URL
Expand All @@ -62,10 +64,16 @@ def get_download_link(
if not py_ver.matches(request, implementation):
continue

matched = urls.get((platform, arch, not build_dir))
if request.endswith("t"):
free_threaded = True

matched = urls.get((platform, arch, not build_dir, free_threaded))
if matched is not None:
return py_ver, matched
if build_dir and (matched := urls.get((platform, arch, False))) is not None:
if (
not build_dir
and (matched := urls.get((platform, arch, False, free_threaded))) is not None
):
return py_ver, matched
raise ValueError(
f"Could not find a version matching version={request!r}, implementation={implementation}"
Expand Down Expand Up @@ -162,6 +170,7 @@ def install(
platform: str | None = None,
implementation: PythonImplementation = "cpython",
build_dir: bool = False,
free_threaded: bool = False,
) -> None:
"""Download and install the requested python version.
Expand All @@ -177,7 +186,7 @@ def install(
platform: The platform to install, e.g. linux, macos
implementation: The implementation of Python to install, allowed values are 'cpython' and 'pypy'
build_dir: Whether to include the `build/` directory from indygreg builds
free_threaded: Whether to install the freethreaded version of Python
Examples:
>>> install("3.10", "./python")
Installing cpython@3.10.4 to ./python
Expand All @@ -190,7 +199,12 @@ def install(
arch = THIS_ARCH

ver, python_file = get_download_link(
request, arch=arch, platform=platform, implementation=implementation, build_dir=build_dir
request,
arch=arch,
platform=platform,
implementation=implementation,
build_dir=build_dir,
free_threaded=free_threaded,
)
if version_dir:
destination = os.path.join(destination, str(ver))
Expand Down
2 changes: 1 addition & 1 deletion src/pbs_installer/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def matches(self, request: str, implementation: str) -> bool:
if implementation != self.implementation:
return False
try:
parts = tuple(int(v) for v in request.split("."))
parts = tuple(int(v) for v in request.rstrip("t").split("."))
except ValueError:
raise ValueError(
f"Invalid version: {request!r}, each part must be an integer"
Expand Down
Loading

0 comments on commit eda2373

Please sign in to comment.