Add support for managed installs of free-threaded Python (#8100)

Closes https://github.com/astral-sh/uv/issues/7193

```

❯ cargo run -q -- python uninstall 3.13t
Searching for Python versions matching: Python 3.13t
Uninstalled Python 3.13.0 in 231ms
 - cpython-3.13.0+freethreaded-macos-aarch64-none
❯ cargo run -q -- python install 3.13t
Searching for Python versions matching: Python 3.13t
Installed Python 3.13.0 in 3.54s
 + cpython-3.13.0+freethreaded-macos-aarch64-none
❯ cargo run -q -- python install 3.12t
Searching for Python versions matching: Python 3.12t
error: No download found for request: cpython-3.12t-macos-aarch64-none
❯ cargo run -q -- python install 3.13rc3t
Searching for Python versions matching: Python 3.13rc3t
Found existing installation for Python 3.13rc3t: cpython-3.13.0+freethreaded-macos-aarch64-none
❯ cargo run -q -- run -p 3.13t python -c "import sys; print(sys.base_prefix)"
/Users/zb/.local/share/uv/python/cpython-3.13.0+freethreaded-macos-aarch64-none
```
This commit is contained in:
Zanie Blue 2024-10-14 15:18:52 -05:00 committed by GitHub
parent db0f0aec09
commit 5f33915e03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 6319 additions and 4211 deletions

View file

@ -106,6 +106,11 @@ class ImplementationName(StrEnum):
PYPY = "pypy"
class Variant(StrEnum):
FREETHREADED = "freethreaded"
DEBUG = "debug"
@dataclass
class PythonDownload:
version: Version
@ -115,9 +120,13 @@ class PythonDownload:
filename: str
url: str
sha256: str | None = None
variant: Variant | None = None
def key(self) -> str:
return f"{self.implementation}-{self.version}-{self.triple.platform}-{self.triple.arch}-{self.triple.libc}"
if self.variant:
return f"{self.implementation}-{self.version}+{self.variant}-{self.triple.platform}-{self.triple.arch}-{self.triple.libc}"
else:
return f"{self.implementation}-{self.version}-{self.triple.platform}-{self.triple.arch}-{self.triple.libc}"
class Finder:
@ -141,13 +150,6 @@ class CPythonFinder(Finder):
"shared-pgo",
"shared-noopt",
"static-noopt",
"pgo+lto",
"pgo",
"lto",
"debug",
]
HIDDEN_FLAVORS = [
"noopt",
]
SPECIAL_TRIPLES = {
"macos": "x86_64-apple-darwin",
@ -167,26 +169,17 @@ class CPythonFinder(Finder):
_filename_re = re.compile(
r"""(?x)
^
cpython-(?P<ver>\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?)
(?:\+\d+)?
-(?P<triple>.*?)
(?:-[\dT]+)?\.tar\.(?:gz|zst)
cpython-
(?P<ver>\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?)(?:\+\d+)?\+
(?P<date>\d+)-
(?P<triple>[a-z\d_]+-[a-z\d]+(?>-[a-z\d]+)?-[a-z\d]+)-
(?>(?P<build_options>.+)-)?
(?P<flavor>.+)
\.tar\.(?:gz|zst)
$
"""
)
_flavor_re = re.compile(
r"""(?x)^(.*?)-(%s)$"""
% (
"|".join(
map(
re.escape,
sorted(FLAVOR_PREFERENCES + HIDDEN_FLAVORS, key=len, reverse=True),
)
)
)
)
def __init__(self, client: httpx.AsyncClient):
self.client = client
@ -197,7 +190,7 @@ class CPythonFinder(Finder):
async def _fetch_downloads(self, pages: int = 100) -> list[PythonDownload]:
"""Fetch all the indygreg downloads from the release API."""
results: dict[Version, list[PythonDownload]] = {}
downloads_by_version: dict[Version, list[PythonDownload]] = {}
# Collect all available Python downloads
for page in range(1, pages + 1):
@ -213,24 +206,39 @@ class CPythonFinder(Finder):
download = self._parse_download_url(url)
if download is None:
continue
results.setdefault(download.version, []).append(download)
logging.debug("Found %s (%s)", download.key(), download.filename)
downloads_by_version.setdefault(download.version, []).append(
download
)
# Collapse CPython variants to a single URL flavor per triple
# Collapse CPython variants to a single URL flavor per triple and variant
downloads = []
for choices in results.values():
flavors: dict[PlatformTriple, tuple[PythonDownload, int]] = {}
for choice in choices:
priority = self._get_flavor_priority(choice.flavor)
existing = flavors.get(choice.triple)
for version_downloads in downloads_by_version.values():
selected: dict[
tuple[PlatformTriple, Variant | None], tuple[PythonDownload, int]
] = {}
for download in version_downloads:
priority = self._get_flavor_priority(download.flavor)
existing = selected.get((download.triple, download.variant))
if existing:
_, existing_priority = existing
existing_download, existing_priority = existing
# Skip if we have a flavor with higher priority already (indicated by a smaller value)
if priority >= existing_priority:
logging.debug(
"Skipping %s (%s): lower priority than %s (%s)",
download.key(),
download.flavor,
existing_download.key(),
existing_download.flavor,
)
continue
flavors[choice.triple] = (choice, priority)
selected[(download.triple, download.variant)] = (
download,
priority,
)
# Drop the priorities
downloads.extend([choice for choice, _ in flavors.values()])
downloads.extend([download for download, _ in selected.values()])
return downloads
@ -288,23 +296,23 @@ class CPythonFinder(Finder):
match = self._filename_re.match(filename)
if match is None:
logging.debug("Skipping %s: no regex match", filename)
return None
version, triple = match.groups()
if triple.endswith("-full"):
triple = triple[:-5]
version, _date, triple, build_options, flavor = match.groups()
match = self._flavor_re.match(triple)
if match is not None:
triple, flavor = match.groups()
variants = build_options.split("+") if build_options else []
variant: Variant | None
for variant in Variant:
if variant in variants:
break
else:
flavor = ""
if flavor in self.HIDDEN_FLAVORS:
return None
variant = None
version = Version.from_str(version)
triple = self._normalize_triple(triple)
if triple is None:
# Skip is logged in `_normalize_triple`
return None
return PythonDownload(
@ -314,6 +322,7 @@ class CPythonFinder(Finder):
implementation=self.implementation,
filename=filename,
url=url,
variant=variant,
)
def _normalize_triple(self, triple: str) -> PlatformTriple | None:
@ -457,6 +466,16 @@ def render(downloads: list[PythonDownload]) -> None:
return 2, int(prerelease[2:])
return 3, 0
def variant_sort_key(variant: Variant | None) -> int:
if variant is None:
return 0
match variant:
case Variant.FREETHREADED:
return 1
case Variant.DEBUG:
return 2
raise ValueError(f"Missing sort key implementation for variant: {variant}")
def sort_key(download: PythonDownload) -> tuple:
# Sort by implementation, version (latest first), and then by triple.
impl_order = [ImplementationName.CPYTHON, ImplementationName.PYPY]
@ -468,6 +487,7 @@ def render(downloads: list[PythonDownload]) -> None:
-download.version.patch,
-prerelease[0],
-prerelease[1],
variant_sort_key(download.variant),
download.triple,
)
@ -477,7 +497,7 @@ def render(downloads: list[PythonDownload]) -> None:
for download in downloads:
key = download.key()
logging.info(
"Found %s%s", key, (" (%s)" % download.flavor) if download.flavor else ""
"Selected %s%s", key, (" (%s)" % download.flavor) if download.flavor else ""
)
results[key] = {
"name": download.implementation,
@ -490,6 +510,7 @@ def render(downloads: list[PythonDownload]) -> None:
"prerelease": download.version.prerelease,
"url": download.url,
"sha256": download.sha256,
"variant": download.variant if download.variant else None,
}
VERSIONS_FILE.parent.mkdir(parents=True, exist_ok=True)