Change "toolchain" to "python" (#4735)

Whew this is a lot.

The user-facing changes are:

- `uv toolchain` to `uv python` e.g. `uv python find`, `uv python
install`, ...
- `UV_TOOLCHAIN_DIR` to` UV_PYTHON_INSTALL_DIR`
- `<UV_STATE_DIR>/toolchains` to `<UV_STATE_DIR>/python` (with
[automatic
migration](https://github.com/astral-sh/uv/pull/4735/files#r1663029330))
- User-facing messages no longer refer to toolchains, instead using
"Python", "Python versions" or "Python installations"

The internal changes are:

- `uv-toolchain` crate to `uv-python`
- `Toolchain` no longer referenced in type names
- Dropped unused `SystemPython` type (previously replaced)
- Clarified the type names for "managed Python installations"
- (more little things)
This commit is contained in:
Zanie Blue 2024-07-03 08:44:29 -04:00 committed by GitHub
parent 60fd98a5e4
commit dd7da6af5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
105 changed files with 2629 additions and 2603 deletions

View file

@ -0,0 +1,63 @@
[package]
name = "uv-python"
version = "0.0.1"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
[lints]
workspace = true
[dependencies]
cache-key = { workspace = true }
install-wheel-rs = { workspace = true }
pep440_rs = { workspace = true }
pep508_rs = { workspace = true }
platform-tags = { workspace = true }
pypi-types = { workspace = true }
uv-cache = { workspace = true }
uv-configuration = { workspace = true }
uv-client = { workspace = true }
uv-extract = { workspace = true }
uv-fs = { workspace = true }
uv-state = { workspace = true }
uv-warnings = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true, optional = true }
configparser = { workspace = true }
fs-err = { workspace = true, features = ["tokio"] }
itertools = { workspace = true }
futures = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
reqwest = { workspace = true }
reqwest-middleware = { workspace = true }
rmp-serde = { workspace = true }
same-file = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
target-lexicon = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio-util = { workspace = true, features = ["compat"] }
tracing = { workspace = true }
url = { workspace = true }
which = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { workspace = true }
[dev-dependencies]
anyhow = { version = "1.0.80" }
assert_fs = { version = "1.1.1" }
indoc = { version = "2.0.4" }
itertools = { version = "0.13.0" }
temp-env = { version = "0.3.6" }
tempfile = { version = "3.9.0" }
test-log = { version = "0.2.15", features = ["trace"], default-features = false }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,289 @@
#!/usr/bin/env python3.12
"""
Fetch Python version download metadata.
Generates the `download-metadata.json` file.
Usage:
python fetch-download-metadata.py
Acknowledgements:
Derived from https://github.com/mitsuhiko/rye/tree/f9822267a7f00332d15be8551f89a212e7bc9017
Originally authored by Armin Ronacher under the MIT license
"""
import argparse
import hashlib
import json
import logging
import re
import urllib.error
import urllib.request
from itertools import chain
from pathlib import Path
from urllib.parse import unquote
SELF_DIR = Path(__file__).parent
RELEASE_URL = "https://api.github.com/repos/indygreg/python-build-standalone/releases"
HEADERS = {
"X-GitHub-Api-Version": "2022-11-28",
}
VERSIONS_FILE = SELF_DIR / "download-metadata.json"
FLAVOR_PREFERENCES = [
"shared-pgo",
"shared-noopt",
"shared-noopt",
"static-noopt",
"gnu-pgo+lto",
"gnu-lto",
"gnu-pgo",
"pgo+lto",
"lto",
"pgo",
]
HIDDEN_FLAVORS = [
"debug",
"noopt",
"install_only",
]
SPECIAL_TRIPLES = {
"macos": "x86_64-apple-darwin",
"linux64": "x86_64-unknown-linux-gnu",
"windows-amd64": "x86_64-pc-windows",
"windows-x86": "i686-pc-windows",
"windows-amd64-shared": "x86_64-pc-windows",
"windows-x86-shared": "i686-pc-windows",
"linux64-musl": "x86_64-unknown-linux-musl",
}
_filename_re = re.compile(
r"""(?x)
^
cpython-(?P<ver>\d+\.\d+\.\d+?)
(?:\+\d+)?
-(?P<triple>.*?)
(?:-[\dT]+)?\.tar\.(?:gz|zst)
$
"""
)
_suffix_re = re.compile(
r"""(?x)^(.*?)-(%s)$"""
% (
"|".join(
map(
re.escape,
sorted(FLAVOR_PREFERENCES + HIDDEN_FLAVORS, key=len, reverse=True),
)
)
)
)
# Normalized mappings to match the Rust types
ARCH_MAP = {
"ppc64": "powerpc64",
"ppc64le": "powerpc64le",
}
def parse_filename(filename):
match = _filename_re.match(filename)
if match is None:
return
version, triple = match.groups()
if triple.endswith("-full"):
triple = triple[:-5]
match = _suffix_re.match(triple)
if match is not None:
triple, suffix = match.groups()
else:
suffix = None
return (version, triple, suffix)
def normalize_triple(triple):
if "-static" in triple:
logging.debug("Skipping %r: static unsupported", triple)
return
triple = SPECIAL_TRIPLES.get(triple, triple)
pieces = triple.split("-")
try:
arch = normalize_arch(pieces[0])
operating_system = normalize_os(pieces[2])
if pieces[2] == "linux":
# On linux, the triple has four segments, the last one is the libc
libc = pieces[3]
else:
libc = "none"
except IndexError:
logging.debug("Skipping %r: unknown triple", triple)
return
return "%s-%s-%s" % (arch, operating_system, libc)
def normalize_arch(arch):
arch = ARCH_MAP.get(arch, arch)
pieces = arch.split("_")
# Strip `_vN` from `x86_64`
return "_".join(pieces[:2])
def normalize_os(os):
return os
def read_sha256(url):
try:
resp = urllib.request.urlopen(url + ".sha256")
except urllib.error.HTTPError:
return None
assert resp.status == 200
return resp.read().decode().strip()
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as file:
while True:
# Reading is buffered, so we can read smaller chunks.
chunk = file.read(h.block_size)
if not chunk:
break
h.update(chunk)
return h.hexdigest()
def _sort_by_flavor_preference(info):
_triple, flavor, _url = info
try:
pref = FLAVOR_PREFERENCES.index(flavor)
except ValueError:
pref = len(FLAVOR_PREFERENCES) + 1
return pref
def _sort_by_interpreter_and_version(info):
interpreter, version_tuple, _ = info
return (interpreter, version_tuple)
def find():
"""
Find available Python versions and write metadata to a file.
"""
results = {}
# Collect all available Python downloads
for page in range(1, 100):
logging.debug("Reading release page %s...", page)
resp = urllib.request.urlopen("%s?page=%d" % (RELEASE_URL, page))
rows = json.loads(resp.read())
if not rows:
break
for row in rows:
for asset in row["assets"]:
url = asset["browser_download_url"]
base_name = unquote(url.rsplit("/")[-1])
if base_name.endswith(".sha256"):
continue
info = parse_filename(base_name)
if info is None:
continue
py_ver, triple, flavor = info
if "-static" in triple or (flavor and "noopt" in flavor):
continue
triple = normalize_triple(triple)
if triple is None:
continue
results.setdefault(py_ver, []).append((triple, flavor, url))
# Collapse CPython variants to a single URL flavor per triple
cpython_results: dict[tuple[int, int, int], dict[tuple[str, str, str], str]] = {}
for py_ver, choices in results.items():
urls = {}
for triple, flavor, url in sorted(choices, key=_sort_by_flavor_preference):
triple = tuple(triple.split("-"))
# Skip existing triples, preferring the first flavor
if triple in urls:
continue
urls[triple] = url
cpython_results[tuple(map(int, py_ver.split(".")))] = urls
# Collect variants across interpreter kinds
# TODO(zanieb): Note we only support CPython downloads at this time
# but this will include PyPy chain in the future.
final_results = {}
for interpreter, py_ver, choices in sorted(
chain(
(("cpython",) + x for x in cpython_results.items()),
),
key=_sort_by_interpreter_and_version,
# Reverse the ordering so newer versions are first
reverse=True,
):
# Sort by the remaining information for determinism
# This groups download metadata in triple component order
for (arch, operating_system, libc), url in sorted(choices.items()):
key = "%s-%s.%s.%s-%s-%s-%s" % (
interpreter,
*py_ver,
operating_system,
arch,
libc,
)
logging.info("Found %s", key)
sha256 = read_sha256(url)
final_results[key] = {
"name": interpreter,
"arch": arch,
"os": operating_system,
"libc": libc,
"major": py_ver[0],
"minor": py_ver[1],
"patch": py_ver[2],
"url": url,
"sha256": sha256,
}
VERSIONS_FILE.parent.mkdir(parents=True, exist_ok=True)
VERSIONS_FILE.write_text(json.dumps(final_results, indent=2))
def main():
parser = argparse.ArgumentParser(description="Fetch Python version metadata.")
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable debug logging",
)
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Disable logging",
)
args = parser.parse_args()
if args.quiet:
log_level = logging.CRITICAL
elif args.verbose:
log_level = logging.DEBUG
else:
log_level = logging.INFO
logging.basicConfig(
level=log_level,
format="%(asctime)s %(levelname)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
find()
if __name__ == "__main__":
main()

View file

View file

@ -0,0 +1,569 @@
"""
Queries information about the current Python interpreter and prints it as JSON.
The script will exit with status 0 on known error that are turned into rust errors.
"""
import sys
import json
import os
import platform
import struct
import sysconfig
def format_full_version(info):
version = "{0.major}.{0.minor}.{0.micro}".format(info)
kind = info.releaselevel
if kind != "final":
version += kind[0] + str(info.serial)
return version
if sys.version_info[0] < 3:
print(
json.dumps(
{
"result": "error",
"kind": "unsupported_python_version",
"python_version": format_full_version(sys.version_info),
}
)
)
sys.exit(0)
if hasattr(sys, "implementation"):
implementation_version = format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name
else:
implementation_version = "0"
implementation_name = ""
python_full_version = platform.python_version()
# For local builds of Python, at time of writing, the version numbers end with
# a `+`. This makes the version non-PEP-440 compatible since a `+` indicates
# the start of a local segment which must be non-empty. Thus, `uv` chokes on it
# and spits out an error[1] when trying to create a venv using a "local" build
# of Python. Arguably, the right fix for this is for CPython to use a PEP-440
# compatible version number[2].
#
# However, as a work-around for now, as suggested by pradyunsg[3] as one
# possible direction forward, we strip the `+`.
#
# This fix does unfortunately mean that one cannot specify a Python version
# constraint that specifically selects a local version[4]. But at the time of
# writing, it seems reasonable to block such functionality on this being fixed
# upstream (in some way).
#
# Another alternative would be to treat such invalid versions as strings (which
# is what PEP-508 suggests), but this leads to undesirable behavior in this
# case. For example, let's say you have a Python constraint of `>=3.9.1` and
# a local build of Python with a version `3.11.1+`. Using string comparisons
# would mean the constraint wouldn't be satisfied:
#
# >>> "3.9.1" < "3.11.1+"
# False
#
# So in the end, we just strip the trailing `+`, as was done in the days of old
# for legacy version numbers[5].
#
# [1]: https://github.com/astral-sh/uv/issues/1357
# [2]: https://github.com/python/cpython/issues/99968
# [3]: https://github.com/pypa/packaging/issues/678#issuecomment-1436033646
# [4]: https://github.com/astral-sh/uv/issues/1357#issuecomment-1947645243
# [5]: https://github.com/pypa/packaging/blob/085ff41692b687ae5b0772a55615b69a5b677be9/packaging/version.py#L168-L193
if len(python_full_version) > 0 and python_full_version[-1] == "+":
python_full_version = python_full_version[:-1]
def _running_under_venv() -> bool:
"""Checks if sys.base_prefix and sys.prefix match.
This handles PEP 405 compliant virtual environments.
"""
return sys.prefix != getattr(sys, "base_prefix", sys.prefix)
def _running_under_legacy_virtualenv() -> bool:
"""Checks if sys.real_prefix is set.
This handles virtual environments created with pypa's virtualenv.
"""
# pypa/virtualenv case
return hasattr(sys, "real_prefix")
def running_under_virtualenv() -> bool:
"""True if we're running inside a virtual environment, False otherwise."""
return _running_under_venv() or _running_under_legacy_virtualenv()
def get_major_minor_version() -> str:
"""
Return the major-minor version of the current Python as a string, e.g.
"3.7" or "3.10".
"""
return "{}.{}".format(*sys.version_info)
def get_virtualenv():
"""Return the expected Scheme for virtualenvs created by this interpreter.
The paths returned should be relative to a root directory.
This is based on virtualenv's path discovery logic:
https://github.com/pypa/virtualenv/blob/5cd543fdf8047600ff2737babec4a635ad74d169/src/virtualenv/discovery/py_info.py#L80C9-L80C17
"""
scheme_names = sysconfig.get_scheme_names()
# Determine the scheme to use, if any.
if "venv" in scheme_names:
sysconfig_scheme = "venv"
elif sys.version_info[:2] == (3, 10) and "deb_system" in scheme_names:
# debian / ubuntu python 3.10 without `python3-distutils` will report
# mangled `local/bin` / etc. names for the default prefix
# intentionally select `posix_prefix` which is the unaltered posix-like paths
sysconfig_scheme = "posix_prefix"
else:
sysconfig_scheme = None
# Use `sysconfig`, if available.
if sysconfig_scheme:
import re
sysconfig_paths = {
i: sysconfig.get_path(i, expand=False, scheme=sysconfig_scheme)
for i in sysconfig.get_path_names()
}
# Determine very configuration variable that we need to resolve.
config_var_keys = set()
conf_var_re = re.compile(r"\{\w+}")
for element in sysconfig_paths.values():
for k in conf_var_re.findall(element):
config_var_keys.add(k[1:-1])
config_var_keys.add("PYTHONFRAMEWORK")
# Look them up.
sysconfig_vars = {i: sysconfig.get_config_var(i or "") for i in config_var_keys}
# Information about the prefix (determines the Python home).
prefix = os.path.abspath(sys.prefix)
base_prefix = os.path.abspath(sys.base_prefix)
# Information about the exec prefix (dynamic stdlib modules).
base_exec_prefix = os.path.abspath(sys.base_exec_prefix)
exec_prefix = os.path.abspath(sys.exec_prefix)
# Set any prefixes to empty, which makes the resulting paths relative.
prefixes = prefix, exec_prefix, base_prefix, base_exec_prefix
sysconfig_vars.update(
{k: "" if v in prefixes else v for k, v in sysconfig_vars.items()}
)
def expand_path(path: str) -> str:
return path.format(**sysconfig_vars).replace("/", os.sep).lstrip(os.sep)
return {
"purelib": expand_path(sysconfig_paths["purelib"]),
"platlib": expand_path(sysconfig_paths["platlib"]),
"include": os.path.join(
"include", "site", f"python{get_major_minor_version()}"
),
"scripts": expand_path(sysconfig_paths["scripts"]),
"data": expand_path(sysconfig_paths["data"]),
}
else:
# Disable the use of the setuptools shim, if it's injected. Per pip:
#
# > If pip's going to use distutils, it should not be using the copy that setuptools
# > might have injected into the environment. This is done by removing the injected
# > shim, if it's injected.
#
# > See https://github.com/pypa/pip/issues/8761 for the original discussion and
# > rationale for why this is done within pip.
try:
__import__("_distutils_hack").remove_shim()
except (ImportError, AttributeError):
pass
# Use distutils primarily because that's what pip does.
# https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/locations/__init__.py#L249
import warnings
with warnings.catch_warnings(): # disable warning for PEP-632
warnings.simplefilter("ignore")
from distutils import dist
from distutils.command.install import SCHEME_KEYS
d = dist.Distribution({"script_args": "--no-user-cfg"})
if hasattr(sys, "_framework"):
sys._framework = None
with warnings.catch_warnings():
warnings.simplefilter("ignore")
i = d.get_command_obj("install", create=True)
i.prefix = os.sep
i.finalize_options()
distutils_paths = {
key: (getattr(i, f"install_{key}")[1:]).lstrip(os.sep)
for key in SCHEME_KEYS
}
return {
"purelib": distutils_paths["purelib"],
"platlib": distutils_paths["platlib"],
"include": os.path.join(
"include", "site", f"python{get_major_minor_version()}"
),
"scripts": distutils_paths["scripts"],
"data": distutils_paths["data"],
}
def get_scheme():
"""Return the Scheme for the current interpreter.
The paths returned should be absolute.
This is based on pip's path discovery logic:
https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/locations/__init__.py#L230
"""
def get_sysconfig_scheme():
"""Get the "scheme" corresponding to the input parameters.
Uses the `sysconfig` module to get the scheme.
Based on (with default arguments):
https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/locations/_sysconfig.py#L124
"""
def is_osx_framework() -> bool:
return bool(sysconfig.get_config_var("PYTHONFRAMEWORK"))
# Notes on _infer_* functions.
# Unfortunately ``get_default_scheme()`` didn't exist before 3.10, so there's no
# way to ask things like "what is the '_prefix' scheme on this platform". These
# functions try to answer that with some heuristics while accounting for ad-hoc
# platforms not covered by CPython's default sysconfig implementation. If the
# ad-hoc implementation does not fully implement sysconfig, we'll fall back to
# a POSIX scheme.
_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names())
_PREFERRED_SCHEME_API = getattr(sysconfig, "get_preferred_scheme", None)
def _should_use_osx_framework_prefix() -> bool:
"""Check for Apple's ``osx_framework_library`` scheme.
Python distributed by Apple's Command Line Tools has this special scheme
that's used when:
* This is a framework build.
* We are installing into the system prefix.
This does not account for ``pip install --prefix`` (also means we're not
installing to the system prefix), which should use ``posix_prefix``, but
logic here means ``_infer_prefix()`` outputs ``osx_framework_library``. But
since ``prefix`` is not available for ``sysconfig.get_default_scheme()``,
which is the stdlib replacement for ``_infer_prefix()``, presumably Apple
wouldn't be able to magically switch between ``osx_framework_library`` and
``posix_prefix``. ``_infer_prefix()`` returning ``osx_framework_library``
means its behavior is consistent whether we use the stdlib implementation
or our own, and we deal with this special case in ``get_scheme()`` instead.
"""
return (
"osx_framework_library" in _AVAILABLE_SCHEMES
and not running_under_virtualenv()
and is_osx_framework()
)
def _infer_prefix() -> str:
"""Try to find a prefix scheme for the current platform.
This tries:
* A special ``osx_framework_library`` for Python distributed by Apple's
Command Line Tools, when not running in a virtual environment.
* Implementation + OS, used by PyPy on Windows (``pypy_nt``).
* Implementation without OS, used by PyPy on POSIX (``pypy``).
* OS + "prefix", used by CPython on POSIX (``posix_prefix``).
* Just the OS name, used by CPython on Windows (``nt``).
If none of the above works, fall back to ``posix_prefix``.
"""
if _PREFERRED_SCHEME_API:
return _PREFERRED_SCHEME_API("prefix")
if _should_use_osx_framework_prefix():
return "osx_framework_library"
implementation_suffixed = f"{sys.implementation.name}_{os.name}"
if implementation_suffixed in _AVAILABLE_SCHEMES:
return implementation_suffixed
if sys.implementation.name in _AVAILABLE_SCHEMES:
return sys.implementation.name
suffixed = f"{os.name}_prefix"
if suffixed in _AVAILABLE_SCHEMES:
return suffixed
if os.name in _AVAILABLE_SCHEMES: # On Windows, prefx is just called "nt".
return os.name
return "posix_prefix"
scheme_name = _infer_prefix()
paths = sysconfig.get_paths(scheme=scheme_name)
# Logic here is very arbitrary, we're doing it for compatibility, don't ask.
# 1. Pip historically uses a special header path in virtual environments.
if running_under_virtualenv():
python_xy = f"python{get_major_minor_version()}"
paths["include"] = os.path.join(sys.prefix, "include", "site", python_xy)
return {
"platlib": paths["platlib"],
"purelib": paths["purelib"],
"include": paths["include"],
"scripts": paths["scripts"],
"data": paths["data"],
}
def get_distutils_scheme():
"""Get the "scheme" corresponding to the input parameters.
Uses the deprecated `distutils` module to get the scheme.
Based on (with default arguments):
https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/locations/_distutils.py#L115
"""
# Disable the use of the setuptools shim, if it's injected. Per pip:
#
# > If pip's going to use distutils, it should not be using the copy that setuptools
# > might have injected into the environment. This is done by removing the injected
# > shim, if it's injected.
#
# > See https://github.com/pypa/pip/issues/8761 for the original discussion and
# > rationale for why this is done within pip.
try:
__import__("_distutils_hack").remove_shim()
except (ImportError, AttributeError):
pass
import warnings
with warnings.catch_warnings(): # disable warning for PEP-632
warnings.simplefilter("ignore")
from distutils.dist import Distribution
dist_args = {}
d = Distribution(dist_args)
try:
d.parse_config_files()
except UnicodeDecodeError:
pass
with warnings.catch_warnings():
warnings.simplefilter("ignore")
i = d.get_command_obj("install", create=True)
i.finalize_options()
scheme = {}
for key in ("purelib", "platlib", "headers", "scripts", "data"):
scheme[key] = getattr(i, "install_" + key)
# install_lib specified in setup.cfg should install *everything*
# into there (i.e. it takes precedence over both purelib and
# platlib). Note, i.install_lib is *always* set after
# finalize_options(); we only want to override here if the user
# has explicitly requested it hence going back to the config
if "install_lib" in d.get_option_dict("install"):
# noinspection PyUnresolvedReferences
scheme.update({"purelib": i.install_lib, "platlib": i.install_lib})
if running_under_virtualenv():
# noinspection PyUnresolvedReferences
scheme["headers"] = os.path.join(
i.prefix,
"include",
"site",
f"python{get_major_minor_version()}",
"UNKNOWN",
)
return {
"platlib": scheme["platlib"],
"purelib": scheme["purelib"],
"include": os.path.dirname(scheme["headers"]),
"scripts": scheme["scripts"],
"data": scheme["data"],
}
# By default, pip uses sysconfig on Python 3.10+.
# But Python distributors can override this decision by setting:
# sysconfig._PIP_USE_SYSCONFIG = True / False
# Rationale in https://github.com/pypa/pip/issues/10647
use_sysconfig = bool(
getattr(sysconfig, "_PIP_USE_SYSCONFIG", sys.version_info >= (3, 10))
)
if use_sysconfig:
return get_sysconfig_scheme()
else:
return get_distutils_scheme()
def get_operating_system_and_architecture():
"""Determine the Python interpreter architecture and operating system.
This can differ from uv's architecture and operating system. For example, Apple
Silicon Macs can run both x86_64 and aarch64 binaries transparently.
"""
# https://github.com/pypa/packaging/blob/cc938f984bbbe43c5734b9656c9837ab3a28191f/src/packaging/_musllinux.py#L84
# Note that this is not `os.name`.
# https://docs.python.org/3/library/sysconfig.html#sysconfig.get_platform
# windows x86 will return win32
platform_info = sysconfig.get_platform().split("-", 1)
if len(platform_info) == 1:
if platform_info[0] == "win32":
operating_system, version_arch = "win", "i386"
else:
# unknown_operating_system will flow to the final error print
operating_system, version_arch = platform_info[0], ""
else:
[operating_system, version_arch] = platform_info
if "-" in version_arch:
# Ex: macosx-11.2-arm64
version, architecture = version_arch.rsplit("-", 1)
else:
# Ex: linux-x86_64
version = None
architecture = version_arch
if operating_system == "linux":
if sys.version_info < (3, 7):
print(
json.dumps(
{
"result": "error",
"kind": "unsupported_python_version",
"python_version": format_full_version(sys.version_info),
}
)
)
sys.exit(0)
# noinspection PyProtectedMember
from .packaging._manylinux import _get_glibc_version
# noinspection PyProtectedMember
from .packaging._musllinux import _get_musl_version
musl_version = _get_musl_version(sys.executable)
glibc_version = _get_glibc_version()
if musl_version:
operating_system = {
"name": "musllinux",
"major": musl_version[0],
"minor": musl_version[1],
}
elif glibc_version != (-1, -1):
operating_system = {
"name": "manylinux",
"major": glibc_version[0],
"minor": glibc_version[1],
}
else:
print(json.dumps({"result": "error", "kind": "libc_not_found"}))
sys.exit(0)
elif operating_system == "win":
operating_system = {
"name": "windows",
}
elif operating_system == "macosx":
# Apparently, Mac OS is reporting i386 sometimes in sysconfig.get_platform even
# though that's not a thing anymore.
# https://github.com/astral-sh/uv/issues/2450
version, _, architecture = platform.mac_ver()
# https://github.com/pypa/packaging/blob/cc938f984bbbe43c5734b9656c9837ab3a28191f/src/packaging/tags.py#L356-L363
is_32bit = struct.calcsize("P") == 4
if is_32bit:
if architecture.startswith("ppc"):
architecture = "ppc"
else:
architecture = "i386"
version = version.split(".")
operating_system = {
"name": "macos",
"major": int(version[0]),
"minor": int(version[1]),
}
elif operating_system in [
"freebsd",
"netbsd",
"openbsd",
"dragonfly",
"illumos",
"haiku",
]:
operating_system = {
"name": operating_system,
"release": version,
}
else:
print(
json.dumps(
{
"result": "error",
"kind": "unknown_operating_system",
"operating_system": operating_system,
}
)
)
sys.exit(0)
return {"os": operating_system, "arch": architecture}
def main() -> None:
markers = {
"implementation_name": implementation_name,
"implementation_version": implementation_version,
"os_name": os.name,
"platform_machine": platform.machine(),
"platform_python_implementation": platform.python_implementation(),
"platform_release": platform.release(),
"platform_system": platform.system(),
"platform_version": platform.version(),
"python_full_version": python_full_version,
"python_version": ".".join(platform.python_version_tuple()[:2]),
"sys_platform": sys.platform,
}
interpreter_info = {
"result": "success",
"markers": markers,
"sys_base_prefix": sys.base_prefix,
"sys_base_exec_prefix": sys.base_exec_prefix,
"sys_prefix": sys.prefix,
"sys_base_executable": getattr(sys, "_base_executable", None),
"sys_executable": sys.executable,
"sys_path": sys.path,
"stdlib": sysconfig.get_path("stdlib"),
"scheme": get_scheme(),
"virtualenv": get_virtualenv(),
"platform": get_operating_system_and_architecture(),
# The `t` abiflag for freethreading Python.
# https://peps.python.org/pep-0703/#build-configuration-changes
"gil_disabled": bool(sysconfig.get_config_var("Py_GIL_DISABLED")),
# Determine if the interpreter is 32-bit or 64-bit.
# https://github.com/python/cpython/blob/b228655c227b2ca298a8ffac44d14ce3d22f6faa/Lib/venv/__init__.py#L136
"pointer_size": "64" if sys.maxsize > 2**32 else "32",
}
print(json.dumps(interpreter_info))
if __name__ == "__main__":
main()

View file

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -0,0 +1,23 @@
Copyright (c) Donald Stufft and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,6 @@
# `pypa/packaging`
This directory contains vendored [pypa/packaging](https://github.com/pypa/packaging) modules as of
[cc938f984bbbe43c5734b9656c9837ab3a28191f](https://github.com/pypa/packaging/tree/cc938f984bbbe43c5734b9656c9837ab3a28191f/src/packaging).
The files are licensed under BSD-2-Clause OR Apache-2.0.

View file

@ -0,0 +1,15 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
__title__ = "packaging"
__summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging"
__version__ = "24.1.dev0"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
__license__ = "BSD-2-Clause or Apache-2.0"
__copyright__ = "2014 %s" % __author__

View file

@ -0,0 +1,110 @@
"""
ELF file parser.
This provides a class ``ELFFile`` that parses an ELF executable in a similar
interface to ``ZipFile``. Only the read interface is implemented.
Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
"""
from __future__ import annotations
import enum
import os
import struct
from typing import IO
class ELFInvalid(ValueError):
pass
class EIClass(enum.IntEnum):
C32 = 1
C64 = 2
class EIData(enum.IntEnum):
Lsb = 1
Msb = 2
class EMachine(enum.IntEnum):
I386 = 3
S390 = 22
Arm = 40
X8664 = 62
AArc64 = 183
class ELFFile:
"""
Representation of an ELF executable.
"""
def __init__(self, f: IO[bytes]) -> None:
self._f = f
try:
ident = self._read("16B")
except struct.error:
raise ELFInvalid("unable to parse identification")
magic = bytes(ident[:4])
if magic != b"\x7fELF":
raise ELFInvalid(f"invalid magic: {magic!r}")
self.capacity = ident[4] # Format for program header (bitness).
self.encoding = ident[5] # Data structure encoding (endianness).
try:
# e_fmt: Format for program header.
# p_fmt: Format for section header.
# p_idx: Indexes to find p_type, p_offset, and p_filesz.
e_fmt, self._p_fmt, self._p_idx = {
(1, 1): ("<HHIIIIIHHH", "<IIIIIIII", (0, 1, 4)), # 32-bit LSB.
(1, 2): (">HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB.
(2, 1): ("<HHIQQQIHHH", "<IIQQQQQQ", (0, 2, 5)), # 64-bit LSB.
(2, 2): (">HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB.
}[(self.capacity, self.encoding)]
except KeyError:
raise ELFInvalid(
f"unrecognized capacity ({self.capacity}) or "
f"encoding ({self.encoding})"
)
try:
(
_,
self.machine, # Architecture type.
_,
_,
self._e_phoff, # Offset of program header.
_,
self.flags, # Processor-specific flags.
_,
self._e_phentsize, # Size of section.
self._e_phnum, # Number of sections.
) = self._read(e_fmt)
except struct.error as e:
raise ELFInvalid("unable to parse machine and section information") from e
def _read(self, fmt: str) -> tuple[int, ...]:
return struct.unpack(fmt, self._f.read(struct.calcsize(fmt)))
@property
def interpreter(self) -> str | None:
"""
The path recorded in the ``PT_INTERP`` section header.
"""
for index in range(self._e_phnum):
self._f.seek(self._e_phoff + self._e_phentsize * index)
try:
data = self._read(self._p_fmt)
except struct.error:
continue
if data[self._p_idx[0]] != 3: # Not PT_INTERP.
continue
self._f.seek(data[self._p_idx[1]])
return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0")
return None

View file

@ -0,0 +1,262 @@
from __future__ import annotations
import collections
import contextlib
import functools
import os
import re
import sys
import warnings
from typing import Generator, Iterator, NamedTuple, Sequence
from ._elffile import EIClass, EIData, ELFFile, EMachine
EF_ARM_ABIMASK = 0xFF000000
EF_ARM_ABI_VER5 = 0x05000000
EF_ARM_ABI_FLOAT_HARD = 0x00000400
# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
# as the type for `path` until then.
@contextlib.contextmanager
def _parse_elf(path: str) -> Generator[ELFFile | None, None, None]:
try:
with open(path, "rb") as f:
yield ELFFile(f)
except (OSError, TypeError, ValueError):
yield None
def _is_linux_armhf(executable: str) -> bool:
# hard-float ABI can be detected from the ELF header of the running
# process
# https://static.docs.arm.com/ihi0044/g/aaelf32.pdf
with _parse_elf(executable) as f:
return (
f is not None
and f.capacity == EIClass.C32
and f.encoding == EIData.Lsb
and f.machine == EMachine.Arm
and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER5
and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD
)
def _is_linux_i686(executable: str) -> bool:
with _parse_elf(executable) as f:
return (
f is not None
and f.capacity == EIClass.C32
and f.encoding == EIData.Lsb
and f.machine == EMachine.I386
)
def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
if "armv7l" in archs:
return _is_linux_armhf(executable)
if "i686" in archs:
return _is_linux_i686(executable)
allowed_archs = {
"x86_64",
"aarch64",
"ppc64",
"ppc64le",
"s390x",
"loongarch64",
"riscv64",
}
return any(arch in allowed_archs for arch in archs)
# If glibc ever changes its major version, we need to know what the last
# minor version was, so we can build the complete list of all versions.
# For now, guess what the highest minor version might be, assume it will
# be 50 for testing. Once this actually happens, update the dictionary
# with the actual value.
_LAST_GLIBC_MINOR: dict[int, int] = collections.defaultdict(lambda: 50)
class _GLibCVersion(NamedTuple):
major: int
minor: int
def _glibc_version_string_confstr() -> str | None:
"""
Primary implementation of glibc_version_string using os.confstr.
"""
# os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
# to be broken or missing. This strategy is used in the standard library
# platform module.
# https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
try:
# Should be a string like "glibc 2.17".
version_string: str | None = os.confstr("CS_GNU_LIBC_VERSION")
assert version_string is not None
_, version = version_string.rsplit()
except (AssertionError, AttributeError, OSError, ValueError):
# os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
return None
return version
def _glibc_version_string_ctypes() -> str | None:
"""
Fallback implementation of glibc_version_string using ctypes.
"""
try:
import ctypes
except ImportError:
return None
# ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
# manpage says, "If filename is NULL, then the returned handle is for the
# main program". This way we can let the linker do the work to figure out
# which libc our process is actually using.
#
# We must also handle the special case where the executable is not a
# dynamically linked executable. This can occur when using musl libc,
# for example. In this situation, dlopen() will error, leading to an
# OSError. Interestingly, at least in the case of musl, there is no
# errno set on the OSError. The single string argument used to construct
# OSError comes from libc itself and is therefore not portable to
# hard code here. In any case, failure to call dlopen() means we
# can proceed, so we bail on our attempt.
try:
process_namespace = ctypes.CDLL(None)
except OSError:
return None
try:
gnu_get_libc_version = process_namespace.gnu_get_libc_version
except AttributeError:
# Symbol doesn't exist -> therefore, we are not linked to
# glibc.
return None
# Call gnu_get_libc_version, which returns a string like "2.5"
gnu_get_libc_version.restype = ctypes.c_char_p
version_str: str = gnu_get_libc_version()
# py2 / py3 compatibility:
if not isinstance(version_str, str):
version_str = version_str.decode("ascii")
return version_str
def _glibc_version_string() -> str | None:
"""Returns glibc version string, or None if not using glibc."""
return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
def _parse_glibc_version(version_str: str) -> tuple[int, int]:
"""Parse glibc version.
We use a regexp instead of str.split because we want to discard any
random junk that might come after the minor version -- this might happen
in patched/forked versions of glibc (e.g. Linaro's version of glibc
uses version strings like "2.20-2014.11"). See gh-3588.
"""
m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str)
if not m:
warnings.warn(
f"Expected glibc version with 2 components major.minor,"
f" got: {version_str}",
RuntimeWarning,
)
return -1, -1
return int(m.group("major")), int(m.group("minor"))
@functools.lru_cache()
def _get_glibc_version() -> tuple[int, int]:
version_str = _glibc_version_string()
if version_str is None:
return (-1, -1)
return _parse_glibc_version(version_str)
# From PEP 513, PEP 600
def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
sys_glibc = _get_glibc_version()
if sys_glibc < version:
return False
# Check for presence of _manylinux module.
try:
import _manylinux
except ImportError:
return True
if hasattr(_manylinux, "manylinux_compatible"):
result = _manylinux.manylinux_compatible(version[0], version[1], arch)
if result is not None:
return bool(result)
return True
if version == _GLibCVersion(2, 5):
if hasattr(_manylinux, "manylinux1_compatible"):
return bool(_manylinux.manylinux1_compatible)
if version == _GLibCVersion(2, 12):
if hasattr(_manylinux, "manylinux2010_compatible"):
return bool(_manylinux.manylinux2010_compatible)
if version == _GLibCVersion(2, 17):
if hasattr(_manylinux, "manylinux2014_compatible"):
return bool(_manylinux.manylinux2014_compatible)
return True
_LEGACY_MANYLINUX_MAP = {
# CentOS 7 w/ glibc 2.17 (PEP 599)
(2, 17): "manylinux2014",
# CentOS 6 w/ glibc 2.12 (PEP 571)
(2, 12): "manylinux2010",
# CentOS 5 w/ glibc 2.5 (PEP 513)
(2, 5): "manylinux1",
}
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
"""Generate manylinux tags compatible to the current platform.
:param archs: Sequence of compatible architectures.
The first one shall be the closest to the actual architecture and be the part of
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
be manylinux-compatible.
:returns: An iterator of compatible manylinux tags.
"""
if not _have_compatible_abi(sys.executable, archs):
return
# Oldest glibc to be supported regardless of architecture is (2, 17).
too_old_glibc2 = _GLibCVersion(2, 16)
if set(archs) & {"x86_64", "i686"}:
# On x86/i686 also oldest glibc to be supported is (2, 5).
too_old_glibc2 = _GLibCVersion(2, 4)
current_glibc = _GLibCVersion(*_get_glibc_version())
glibc_max_list = [current_glibc]
# We can assume compatibility across glibc major versions.
# https://sourceware.org/bugzilla/show_bug.cgi?id=24636
#
# Build a list of maximum glibc versions so that we can
# output the canonical list of all glibc from current_glibc
# down to too_old_glibc2, including all intermediary versions.
for glibc_major in range(current_glibc.major - 1, 1, -1):
glibc_minor = _LAST_GLIBC_MINOR[glibc_major]
glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor))
for arch in archs:
for glibc_max in glibc_max_list:
if glibc_max.major == too_old_glibc2.major:
min_minor = too_old_glibc2.minor
else:
# For other glibc major versions oldest supported is (x, 0).
min_minor = -1
for glibc_minor in range(glibc_max.minor, min_minor, -1):
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
tag = "manylinux_{}_{}".format(*glibc_version)
if _is_compatible(arch, glibc_version):
yield f"{tag}_{arch}"
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
if glibc_version in _LEGACY_MANYLINUX_MAP:
legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
if _is_compatible(arch, glibc_version):
yield f"{legacy_tag}_{arch}"

View file

@ -0,0 +1,85 @@
"""PEP 656 support.
This module implements logic to detect if the currently running Python is
linked against musl, and what musl version is used.
"""
from __future__ import annotations
import functools
import re
import subprocess
import sys
from typing import Iterator, NamedTuple, Sequence
from ._elffile import ELFFile
class _MuslVersion(NamedTuple):
major: int
minor: int
def _parse_musl_version(output: str) -> _MuslVersion | None:
lines = [n for n in (n.strip() for n in output.splitlines()) if n]
if len(lines) < 2 or lines[0][:4] != "musl":
return None
m = re.match(r"Version (\d+)\.(\d+)", lines[1])
if not m:
return None
return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
@functools.lru_cache()
def _get_musl_version(executable: str) -> _MuslVersion | None:
"""Detect currently-running musl runtime version.
This is done by checking the specified executable's dynamic linking
information, and invoking the loader to parse its output for a version
string. If the loader is musl, the output would be something like::
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader
"""
try:
with open(executable, "rb") as f:
ld = ELFFile(f).interpreter
except (OSError, TypeError, ValueError):
return None
if ld is None or "musl" not in ld:
return None
proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True)
return _parse_musl_version(proc.stderr)
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
"""Generate musllinux tags compatible to the current platform.
:param archs: Sequence of compatible architectures.
The first one shall be the closest to the actual architecture and be the part of
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
be musllinux-compatible.
:returns: An iterator of compatible musllinux tags.
"""
sys_musl = _get_musl_version(sys.executable)
if sys_musl is None: # Python not dynamically linked against musl.
return
for arch in archs:
for minor in range(sys_musl.minor, -1, -1):
yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
if __name__ == "__main__": # pragma: no cover
import sysconfig
plat = sysconfig.get_platform()
assert plat.startswith("linux-"), "not linux"
print("plat:", plat)
print("musl:", _get_musl_version(sys.executable))
print("tags:", end=" ")
for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])):
print(t, end="\n ")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
// DO NOT EDIT
//
// Generated with `{{generated_with}}`
// From template at `{{generated_from}}`
pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[
{{#versions}}
ManagedPythonDownload {
key: PythonInstallationKey {
major: {{value.major}},
minor: {{value.minor}},
patch: {{value.patch}},
implementation: LenientImplementationName::Known(ImplementationName::{{value.name}}),
arch: Arch(target_lexicon::Architecture::{{value.arch}}),
os: Os(target_lexicon::OperatingSystem::{{value.os}}),
{{#value.libc}}
libc: Libc::Some(target_lexicon::Environment::{{.}}),
{{/value.libc}}
{{^value.libc}}
libc: Libc::None,
{{/value.libc}}
},
url: "{{value.url}}",
{{#value.sha256}}
sha256: Some("{{.}}")
{{/value.sha256}}
{{^value.sha256}}
sha256: None
{{/value.sha256}}
},
{{/versions}}
];

View file

@ -0,0 +1,475 @@
use std::fmt::Display;
use std::io;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use crate::implementation::{
Error as ImplementationError, ImplementationName, LenientImplementationName,
};
use crate::installation::PythonInstallationKey;
use crate::platform::{self, Arch, Libc, Os};
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
use thiserror::Error;
use uv_client::WrappedReqwestError;
use futures::TryStreamExt;
use tokio_util::compat::FuturesAsyncReadCompatExt;
use tracing::{debug, instrument};
use url::Url;
use uv_fs::{rename_with_retry, Simplified};
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
IO(#[from] io::Error),
#[error(transparent)]
ImplementationError(#[from] ImplementationError),
#[error("Invalid python version: {0}")]
InvalidPythonVersion(String),
#[error("Invalid request key, too many parts: {0}")]
TooManyParts(String),
#[error("Download failed")]
NetworkError(#[from] WrappedReqwestError),
#[error("Download failed")]
NetworkMiddlewareError(#[source] anyhow::Error),
#[error("Failed to extract archive: {0}")]
ExtractError(String, #[source] uv_extract::Error),
#[error("Invalid download url")]
InvalidUrl(#[from] url::ParseError),
#[error("Failed to create download directory")]
DownloadDirError(#[source] io::Error),
#[error("Failed to copy to: {0}", to.user_display())]
CopyError {
to: PathBuf,
#[source]
err: io::Error,
},
#[error("Failed to read managed Python installation directory: {0}", dir.user_display())]
ReadError {
dir: PathBuf,
#[source]
err: io::Error,
},
#[error("Failed to parse managed Python directory name: {0}")]
NameError(String),
#[error("Failed to parse request part")]
InvalidRequestPlatform(#[from] platform::Error),
#[error("Cannot download managed Python for request: {0}")]
InvalidRequestKind(PythonRequest),
// TODO(zanieb): Implement display for `PythonDownloadRequest`
#[error("No download found for request: {0:?}")]
NoDownloadFound(PythonDownloadRequest),
}
#[derive(Debug, PartialEq)]
pub struct ManagedPythonDownload {
key: PythonInstallationKey,
url: &'static str,
sha256: Option<&'static str>,
}
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct PythonDownloadRequest {
version: Option<VersionRequest>,
implementation: Option<ImplementationName>,
arch: Option<Arch>,
os: Option<Os>,
libc: Option<Libc>,
}
impl PythonDownloadRequest {
pub fn new(
version: Option<VersionRequest>,
implementation: Option<ImplementationName>,
arch: Option<Arch>,
os: Option<Os>,
libc: Option<Libc>,
) -> Self {
Self {
version,
implementation,
arch,
os,
libc,
}
}
#[must_use]
pub fn with_implementation(mut self, implementation: ImplementationName) -> Self {
self.implementation = Some(implementation);
self
}
#[must_use]
pub fn with_version(mut self, version: VersionRequest) -> Self {
self.version = Some(version);
self
}
#[must_use]
pub fn with_arch(mut self, arch: Arch) -> Self {
self.arch = Some(arch);
self
}
#[must_use]
pub fn with_os(mut self, os: Os) -> Self {
self.os = Some(os);
self
}
#[must_use]
pub fn with_libc(mut self, libc: Libc) -> Self {
self.libc = Some(libc);
self
}
/// Construct a new [`PythonDownloadRequest`] from a [`PythonRequest`] if possible.
///
/// Returns [`None`] if the request kind is not compatible with a download, e.g., it is
/// a request for a specific directory or executable name.
pub fn try_from_request(request: &PythonRequest) -> Option<Self> {
Self::from_request(request).ok()
}
/// Construct a new [`PythonDownloadRequest`] from a [`PythonRequest`].
pub fn from_request(request: &PythonRequest) -> Result<Self, Error> {
match request {
PythonRequest::Version(version) => Ok(Self::default().with_version(version.clone())),
PythonRequest::Implementation(implementation) => {
Ok(Self::default().with_implementation(*implementation))
}
PythonRequest::ImplementationVersion(implementation, version) => Ok(Self::default()
.with_implementation(*implementation)
.with_version(version.clone())),
PythonRequest::Key(request) => Ok(request.clone()),
PythonRequest::Any => Ok(Self::default()),
// We can't download a managed installation for these request kinds
PythonRequest::Directory(_)
| PythonRequest::ExecutableName(_)
| PythonRequest::File(_) => Err(Error::InvalidRequestKind(request.clone())),
}
}
/// Fill empty entries with default values.
///
/// Platform information is pulled from the environment.
#[must_use]
pub fn fill(mut self) -> Self {
if self.implementation.is_none() {
self.implementation = Some(ImplementationName::CPython);
}
if self.arch.is_none() {
self.arch = Some(Arch::from_env());
}
if self.os.is_none() {
self.os = Some(Os::from_env());
}
if self.libc.is_none() {
self.libc = Some(Libc::from_env());
}
self
}
/// Construct a new [`PythonDownloadRequest`] with platform information from the environment.
pub fn from_env() -> Result<Self, Error> {
Ok(Self::new(
None,
None,
Some(Arch::from_env()),
Some(Os::from_env()),
Some(Libc::from_env()),
))
}
pub fn implementation(&self) -> Option<&ImplementationName> {
self.implementation.as_ref()
}
pub fn version(&self) -> Option<&VersionRequest> {
self.version.as_ref()
}
pub fn arch(&self) -> Option<&Arch> {
self.arch.as_ref()
}
pub fn os(&self) -> Option<&Os> {
self.os.as_ref()
}
pub fn libc(&self) -> Option<&Libc> {
self.libc.as_ref()
}
/// Iterate over all [`PythonDownload`]'s that match this request.
pub fn iter_downloads(&self) -> impl Iterator<Item = &'static ManagedPythonDownload> + '_ {
ManagedPythonDownload::iter_all()
.filter(move |download| self.satisfied_by_download(download))
}
pub fn satisfied_by_key(&self, key: &PythonInstallationKey) -> bool {
if let Some(arch) = &self.arch {
if key.arch != *arch {
return false;
}
}
if let Some(os) = &self.os {
if key.os != *os {
return false;
}
}
if let Some(implementation) = &self.implementation {
if key.implementation != LenientImplementationName::from(*implementation) {
return false;
}
}
if let Some(version) = &self.version {
if !version.matches_major_minor_patch(key.major, key.minor, key.patch) {
return false;
}
}
true
}
pub fn satisfied_by_download(&self, download: &ManagedPythonDownload) -> bool {
self.satisfied_by_key(download.key())
}
pub fn satisfied_by_interpreter(&self, interpreter: &Interpreter) -> bool {
if let Some(version) = self.version() {
if !version.matches_interpreter(interpreter) {
return false;
}
}
if let Some(os) = self.os() {
if &Os::from(interpreter.platform().os()) != os {
return false;
}
}
if let Some(arch) = self.arch() {
if &Arch::from(&interpreter.platform().arch()) != arch {
return false;
}
}
if let Some(implementation) = self.implementation() {
if LenientImplementationName::from(interpreter.implementation_name())
!= LenientImplementationName::from(*implementation)
{
return false;
}
}
if let Some(libc) = self.libc() {
if &Libc::from(interpreter.platform().os()) != libc {
return false;
}
}
true
}
}
impl Display for PythonDownloadRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut parts = Vec::new();
if let Some(implementation) = self.implementation {
parts.push(implementation.to_string());
} else {
parts.push("any".to_string());
}
if let Some(version) = &self.version {
parts.push(version.to_string());
} else {
parts.push("any".to_string());
}
if let Some(os) = &self.os {
parts.push(os.to_string());
} else {
parts.push("any".to_string());
}
if let Some(arch) = self.arch {
parts.push(arch.to_string());
} else {
parts.push("any".to_string());
}
if let Some(libc) = self.libc {
parts.push(libc.to_string());
} else {
parts.push("any".to_string());
}
write!(f, "{}", parts.join("-"))
}
}
impl FromStr for PythonDownloadRequest {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split('-');
let mut version = None;
let mut implementation = None;
let mut os = None;
let mut arch = None;
let mut libc = None;
loop {
// Consume each part
let Some(part) = parts.next() else { break };
if implementation.is_none() {
implementation = Some(ImplementationName::from_str(part)?);
continue;
}
if version.is_none() {
version = Some(
VersionRequest::from_str(part)
.map_err(|_| Error::InvalidPythonVersion(part.to_string()))?,
);
continue;
}
if os.is_none() {
os = Some(Os::from_str(part)?);
continue;
}
if arch.is_none() {
arch = Some(Arch::from_str(part)?);
continue;
}
if libc.is_none() {
libc = Some(Libc::from_str(part)?);
continue;
}
return Err(Error::TooManyParts(s.to_string()));
}
Ok(Self::new(version, implementation, arch, os, libc))
}
}
include!("downloads.inc");
pub enum DownloadResult {
AlreadyAvailable(PathBuf),
Fetched(PathBuf),
}
impl ManagedPythonDownload {
/// Return the first [`PythonDownload`] matching a request, if any.
pub fn from_request(
request: &PythonDownloadRequest,
) -> Result<&'static ManagedPythonDownload, Error> {
request
.iter_downloads()
.next()
.ok_or(Error::NoDownloadFound(request.clone()))
}
/// Iterate over all [`PythonDownload`]'s.
pub fn iter_all() -> impl Iterator<Item = &'static ManagedPythonDownload> {
PYTHON_DOWNLOADS.iter()
}
pub fn url(&self) -> &str {
self.url
}
pub fn key(&self) -> &PythonInstallationKey {
&self.key
}
pub fn os(&self) -> &Os {
self.key.os()
}
pub fn sha256(&self) -> Option<&str> {
self.sha256
}
/// Download and extract
#[instrument(skip(client, parent_path), fields(download = %self.key()))]
pub async fn fetch(
&self,
client: &uv_client::BaseClient,
parent_path: &Path,
) -> Result<DownloadResult, Error> {
let url = Url::parse(self.url)?;
let path = parent_path.join(self.key().to_string()).clone();
// If it already exists, return it
if path.is_dir() {
return Ok(DownloadResult::AlreadyAvailable(path));
}
let filename = url.path_segments().unwrap().last().unwrap();
let response = client.get(url.clone()).send().await?;
// Ensure the request was successful.
response.error_for_status_ref()?;
// Download and extract into a temporary directory.
let temp_dir = tempfile::tempdir_in(parent_path).map_err(Error::DownloadDirError)?;
debug!(
"Downloading {url} to temporary location {}",
temp_dir.path().display()
);
let reader = response
.bytes_stream()
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
.into_async_read();
debug!("Extracting {filename}");
uv_extract::stream::archive(reader.compat(), filename, temp_dir.path())
.await
.map_err(|err| Error::ExtractError(filename.to_string(), err))?;
// Extract the top-level directory.
let extracted = match uv_extract::strip_component(temp_dir.path()) {
Ok(top_level) => top_level,
Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.into_path(),
Err(err) => return Err(Error::ExtractError(filename.to_string(), err)),
};
// Persist it to the target
debug!("Moving {} to {}", extracted.display(), path.user_display());
rename_with_retry(extracted, &path)
.await
.map_err(|err| Error::CopyError {
to: path.clone(),
err,
})?;
Ok(DownloadResult::Fetched(path))
}
pub fn python_version(&self) -> PythonVersion {
self.key.version()
}
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Self::NetworkError(WrappedReqwestError::from(error))
}
}
impl From<reqwest_middleware::Error> for Error {
fn from(error: reqwest_middleware::Error) -> Self {
match error {
reqwest_middleware::Error::Middleware(error) => Self::NetworkMiddlewareError(error),
reqwest_middleware::Error::Reqwest(error) => {
Self::NetworkError(WrappedReqwestError::from(error))
}
}
}
}
impl Display for ManagedPythonDownload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.key)
}
}

View file

@ -0,0 +1,233 @@
use std::borrow::Cow;
use std::env;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use uv_cache::Cache;
use uv_fs::{LockedFile, Simplified};
use crate::discovery::find_python_installation;
use crate::installation::PythonInstallation;
use crate::virtualenv::{virtualenv_python_executable, PyVenvConfiguration};
use crate::{
EnvironmentPreference, Error, Interpreter, Prefix, PythonNotFound, PythonPreference,
PythonRequest, Target,
};
/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
#[derive(Debug, Clone)]
pub struct PythonEnvironment(Arc<PythonEnvironmentShared>);
#[derive(Debug, Clone)]
struct PythonEnvironmentShared {
root: PathBuf,
interpreter: Interpreter,
}
/// The result of failed environment discovery.
///
/// Generally this is cast from [`PythonNotFound`] by [`PythonEnvironment::find`].
#[derive(Clone, Debug, Error)]
pub struct EnvironmentNotFound {
request: PythonRequest,
preference: EnvironmentPreference,
}
impl From<PythonNotFound> for EnvironmentNotFound {
fn from(value: PythonNotFound) -> Self {
Self {
request: value.request,
preference: value.environment_preference,
}
}
}
impl fmt::Display for EnvironmentNotFound {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let environment = match self.preference {
EnvironmentPreference::Any => "virtual or system environment",
EnvironmentPreference::ExplicitSystem => {
if self.request.is_explicit_system() {
"virtual or system environment"
} else {
// TODO(zanieb): We could add a hint to use the `--system` flag here
"virtual environment"
}
}
EnvironmentPreference::OnlySystem => "system environment",
EnvironmentPreference::OnlyVirtual => "virtual environment",
};
match self.request {
PythonRequest::Any => {
write!(f, "No {environment} found")
}
_ => {
write!(f, "No {environment} found for {}", self.request)
}
}
}
}
impl PythonEnvironment {
/// Find a [`PythonEnvironment`] matching the given request and preference.
///
/// If looking for a Python interpreter to create a new environment, use [`PythonInstallation::find`]
/// instead.
pub fn find(
request: &PythonRequest,
preference: EnvironmentPreference,
cache: &Cache,
) -> Result<Self, Error> {
let installation = match find_python_installation(
request,
preference,
// Ignore managed installations when looking for environments
PythonPreference::OnlySystem,
cache,
)? {
Ok(installation) => installation,
Err(err) => return Err(EnvironmentNotFound::from(err).into()),
};
Ok(Self::from_installation(installation))
}
/// Create a [`PythonEnvironment`] from the virtual environment at the given root.
pub fn from_root(root: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
let venv = match fs_err::canonicalize(root.as_ref()) {
Ok(venv) => venv,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::MissingEnvironment(EnvironmentNotFound {
preference: EnvironmentPreference::Any,
request: PythonRequest::Directory(root.as_ref().to_owned()),
}));
}
Err(err) => return Err(Error::Discovery(err.into())),
};
let executable = virtualenv_python_executable(venv);
let interpreter = Interpreter::query(executable, cache)?;
Ok(Self(Arc::new(PythonEnvironmentShared {
root: interpreter.sys_prefix().to_path_buf(),
interpreter,
})))
}
/// Create a [`PythonEnvironment`] from an existing [`PythonInstallation`].
pub fn from_installation(installation: PythonInstallation) -> Self {
Self::from_interpreter(installation.into_interpreter())
}
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`].
pub fn from_interpreter(interpreter: Interpreter) -> Self {
Self(Arc::new(PythonEnvironmentShared {
root: interpreter.sys_prefix().to_path_buf(),
interpreter,
}))
}
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and `--target` directory.
#[must_use]
pub fn with_target(self, target: Target) -> Self {
let inner = Arc::unwrap_or_clone(self.0);
Self(Arc::new(PythonEnvironmentShared {
interpreter: inner.interpreter.with_target(target),
..inner
}))
}
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and `--prefix` directory.
#[must_use]
pub fn with_prefix(self, prefix: Prefix) -> Self {
let inner = Arc::unwrap_or_clone(self.0);
Self(Arc::new(PythonEnvironmentShared {
interpreter: inner.interpreter.with_prefix(prefix),
..inner
}))
}
/// Returns the root (i.e., `prefix`) of the Python interpreter.
pub fn root(&self) -> &Path {
&self.0.root
}
/// Return the [`Interpreter`] for this virtual environment.
///
/// See also [`PythonEnvironment::into_interpreter`].
pub fn interpreter(&self) -> &Interpreter {
&self.0.interpreter
}
/// Return the [`PyVenvConfiguration`] for this environment, as extracted from the
/// `pyvenv.cfg` file.
pub fn cfg(&self) -> Result<PyVenvConfiguration, Error> {
Ok(PyVenvConfiguration::parse(self.0.root.join("pyvenv.cfg"))?)
}
/// Returns the location of the Python executable.
pub fn python_executable(&self) -> &Path {
self.0.interpreter.sys_executable()
}
/// Returns an iterator over the `site-packages` directories inside the environment.
///
/// In most cases, `purelib` and `platlib` will be the same, and so the iterator will contain
/// a single element; however, in some distributions, they may be different.
///
/// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
/// still deduplicate the entries, returning a single path.
pub fn site_packages(&self) -> impl Iterator<Item = Cow<Path>> {
let target = self.0.interpreter.target().map(Target::site_packages);
let prefix = self
.0
.interpreter
.prefix()
.map(|prefix| prefix.site_packages(self.0.interpreter.virtualenv()));
let interpreter = if target.is_none() && prefix.is_none() {
Some(self.0.interpreter.site_packages())
} else {
None
};
target
.into_iter()
.flatten()
.map(Cow::Borrowed)
.chain(prefix.into_iter().flatten().map(Cow::Owned))
.chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
}
/// Returns the path to the `bin` directory inside this environment.
pub fn scripts(&self) -> &Path {
self.0.interpreter.scripts()
}
/// Grab a file lock for the environment to prevent concurrent writes across processes.
pub fn lock(&self) -> Result<LockedFile, std::io::Error> {
if let Some(target) = self.0.interpreter.target() {
// If we're installing into a `--target`, use a target-specific lock file.
LockedFile::acquire(target.root().join(".lock"), target.root().user_display())
} else if let Some(prefix) = self.0.interpreter.prefix() {
// Likewise, if we're installing into a `--prefix`, use a prefix-specific lock file.
LockedFile::acquire(prefix.root().join(".lock"), prefix.root().user_display())
} else if self.0.interpreter.is_virtualenv() {
// If the environment a virtualenv, use a virtualenv-specific lock file.
LockedFile::acquire(self.0.root.join(".lock"), self.0.root.user_display())
} else {
// Otherwise, use a global lock file.
LockedFile::acquire(
env::temp_dir().join(format!("uv-{}.lock", cache_key::digest(&self.0.root))),
self.0.root.user_display(),
)
}
}
/// Return the [`Interpreter`] for this environment.
///
/// See also [`PythonEnvironment::interpreter`].
pub fn into_interpreter(self) -> Interpreter {
Arc::unwrap_or_clone(self.0).interpreter
}
}

View file

@ -0,0 +1,109 @@
use std::{
fmt::{self, Display},
str::FromStr,
};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Unknown Python implementation `{0}`")]
UnknownImplementation(String),
}
#[derive(Debug, Eq, PartialEq, Clone, Copy, Default, PartialOrd, Ord)]
pub enum ImplementationName {
#[default]
CPython,
PyPy,
}
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd)]
pub enum LenientImplementationName {
Known(ImplementationName),
Unknown(String),
}
impl ImplementationName {
pub(crate) fn possible_names() -> impl Iterator<Item = &'static str> {
["cpython", "pypy", "cp", "pp"].into_iter()
}
pub fn pretty(self) -> &'static str {
match self {
Self::CPython => "CPython",
Self::PyPy => "PyPy",
}
}
}
impl LenientImplementationName {
pub fn pretty(&self) -> &str {
match self {
Self::Known(implementation) => implementation.pretty(),
Self::Unknown(name) => name,
}
}
}
impl From<&ImplementationName> for &'static str {
fn from(v: &ImplementationName) -> &'static str {
match v {
ImplementationName::CPython => "cpython",
ImplementationName::PyPy => "pypy",
}
}
}
impl<'a> From<&'a LenientImplementationName> for &'a str {
fn from(v: &'a LenientImplementationName) -> &'a str {
match v {
LenientImplementationName::Known(implementation) => implementation.into(),
LenientImplementationName::Unknown(name) => name,
}
}
}
impl FromStr for ImplementationName {
type Err = Error;
/// Parse a Python implementation name from a string.
///
/// Supports the full name and the platform compatibility tag style name.
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"cpython" | "cp" => Ok(Self::CPython),
"pypy" | "pp" => Ok(Self::PyPy),
_ => Err(Error::UnknownImplementation(s.to_string())),
}
}
}
impl Display for ImplementationName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.into())
}
}
impl From<&str> for LenientImplementationName {
fn from(s: &str) -> Self {
match ImplementationName::from_str(s) {
Ok(implementation) => Self::Known(implementation),
Err(_) => Self::Unknown(s.to_string()),
}
}
}
impl From<ImplementationName> for LenientImplementationName {
fn from(implementation: ImplementationName) -> Self {
Self::Known(implementation)
}
}
impl Display for LenientImplementationName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Known(implementation) => implementation.fmt(f),
Self::Unknown(name) => f.write_str(&name.to_ascii_lowercase()),
}
}
}

View file

@ -0,0 +1,342 @@
use std::fmt;
use std::str::FromStr;
use pep440_rs::Version;
use tracing::{debug, info};
use uv_client::BaseClientBuilder;
use uv_cache::Cache;
use crate::discovery::{
find_best_python_installation, find_python_installation, EnvironmentPreference, PythonRequest,
};
use crate::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest};
use crate::implementation::LenientImplementationName;
use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
use crate::platform::{Arch, Libc, Os};
use crate::{Error, Interpreter, PythonFetch, PythonPreference, PythonSource, PythonVersion};
/// A Python interpreter and accompanying tools.
#[derive(Clone, Debug)]
pub struct PythonInstallation {
// Public in the crate for test assertions
pub(crate) source: PythonSource,
pub(crate) interpreter: Interpreter,
}
impl PythonInstallation {
/// Create a new [`PythonInstallation`] from a source, interpreter tuple.
pub(crate) fn from_tuple(tuple: (PythonSource, Interpreter)) -> Self {
let (source, interpreter) = tuple;
Self {
source,
interpreter,
}
}
/// Find an installed [`PythonInstallation`].
///
/// This is the standard interface for discovering a Python installation for creating
/// an environment. If interested in finding an existing environment, see
/// [`PythonEnvironment::find`] instead.
///
/// Note we still require an [`EnvironmentPreference`] as this can either bypass virtual environments
/// or prefer them. In most cases, this should be [`EnvironmentPreference::OnlySystem`]
/// but if you want to allow an interpreter from a virtual environment if it satisfies the request,
/// then use [`EnvironmentPreference::Any`].
///
/// See [`find_installation`] for implementation details.
pub fn find(
request: &PythonRequest,
environments: EnvironmentPreference,
preference: PythonPreference,
cache: &Cache,
) -> Result<Self, Error> {
let installation = find_python_installation(request, environments, preference, cache)??;
Ok(installation)
}
/// Find an installed [`PythonInstallation`] that satisfies a requested version, if the request cannot
/// be satisfied, fallback to the best available Python installation.
pub fn find_best(
request: &PythonRequest,
environments: EnvironmentPreference,
preference: PythonPreference,
cache: &Cache,
) -> Result<Self, Error> {
Ok(find_best_python_installation(
request,
environments,
preference,
cache,
)??)
}
/// Find or fetch a [`PythonInstallation`].
///
/// Unlike [`PythonInstallation::find`], if the required Python is not installed it will be installed automatically.
pub async fn find_or_fetch<'a>(
request: Option<PythonRequest>,
environments: EnvironmentPreference,
preference: PythonPreference,
python_fetch: PythonFetch,
client_builder: &BaseClientBuilder<'a>,
cache: &Cache,
) -> Result<Self, Error> {
let request = request.unwrap_or_default();
// Perform a fetch aggressively if managed Python is preferred
if matches!(preference, PythonPreference::Managed) && python_fetch.is_automatic() {
if let Some(request) = PythonDownloadRequest::try_from_request(&request) {
return Self::fetch(request, client_builder, cache).await;
}
}
// Search for the installation
match Self::find(&request, environments, preference, cache) {
Ok(venv) => Ok(venv),
// If missing and allowed, perform a fetch
err @ Err(Error::MissingPython(_))
if preference.allows_managed()
&& python_fetch.is_automatic()
&& client_builder.connectivity.is_online() =>
{
if let Some(request) = PythonDownloadRequest::try_from_request(&request) {
debug!("Requested Python not found, checking for available download...");
Self::fetch(request, client_builder, cache).await
} else {
err
}
}
Err(err) => Err(err),
}
}
/// Download and install the requested installation.
pub async fn fetch<'a>(
request: PythonDownloadRequest,
client_builder: &BaseClientBuilder<'a>,
cache: &Cache,
) -> Result<Self, Error> {
let installations = ManagedPythonInstallations::from_settings()?.init()?;
let installations_dir = installations.root();
let _lock = installations.acquire_lock()?;
let download = ManagedPythonDownload::from_request(&request)?;
let client = client_builder.build();
info!("Fetching requested Python...");
let result = download.fetch(&client, installations_dir).await?;
let path = match result {
DownloadResult::AlreadyAvailable(path) => path,
DownloadResult::Fetched(path) => path,
};
let installed = ManagedPythonInstallation::new(path)?;
installed.ensure_externally_managed()?;
Ok(Self {
source: PythonSource::Managed,
interpreter: Interpreter::query(installed.executable(), cache)?,
})
}
/// Create a [`PythonInstallation`] from an existing [`Interpreter`].
pub fn from_interpreter(interpreter: Interpreter) -> Self {
Self {
source: PythonSource::ProvidedPath,
interpreter,
}
}
/// Return the [`PythonSource`] of the Python installation, indicating where it was found.
pub fn source(&self) -> &PythonSource {
&self.source
}
pub fn key(&self) -> PythonInstallationKey {
PythonInstallationKey::new(
LenientImplementationName::from(self.interpreter.implementation_name()),
self.interpreter.python_major(),
self.interpreter.python_minor(),
self.interpreter.python_patch(),
self.os(),
self.arch(),
self.libc(),
)
}
/// Return the Python [`Version`] of the Python installation as reported by its interpreter.
pub fn python_version(&self) -> &Version {
self.interpreter.python_version()
}
/// Return the [`LenientImplementationName`] of the Python installation as reported by its interpreter.
pub fn implementation(&self) -> LenientImplementationName {
LenientImplementationName::from(self.interpreter.implementation_name())
}
/// Return the [`Arch`] of the Python installation as reported by its interpreter.
pub fn arch(&self) -> Arch {
Arch::from(&self.interpreter.platform().arch())
}
/// Return the [`Libc`] of the Python installation as reported by its interpreter.
pub fn libc(&self) -> Libc {
Libc::from(self.interpreter.platform().os())
}
/// Return the [`Os`] of the Python installation as reported by its interpreter.
pub fn os(&self) -> Os {
Os::from(self.interpreter.platform().os())
}
/// Return the [`Interpreter`] for the Python installation.
pub fn interpreter(&self) -> &Interpreter {
&self.interpreter
}
pub fn into_interpreter(self) -> Interpreter {
self.interpreter
}
}
#[derive(Error, Debug)]
pub enum PythonInstallationKeyError {
#[error("Failed to parse Python installation key `{0}`: {1}")]
ParseError(String, String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PythonInstallationKey {
pub(crate) implementation: LenientImplementationName,
pub(crate) major: u8,
pub(crate) minor: u8,
pub(crate) patch: u8,
pub(crate) os: Os,
pub(crate) arch: Arch,
pub(crate) libc: Libc,
}
impl PythonInstallationKey {
pub fn new(
implementation: LenientImplementationName,
major: u8,
minor: u8,
patch: u8,
os: Os,
arch: Arch,
libc: Libc,
) -> Self {
Self {
implementation,
major,
minor,
patch,
os,
arch,
libc,
}
}
pub fn implementation(&self) -> &LenientImplementationName {
&self.implementation
}
pub fn version(&self) -> PythonVersion {
PythonVersion::from_str(&format!("{}.{}.{}", self.major, self.minor, self.patch))
.expect("Python installation keys must have valid Python versions")
}
pub fn arch(&self) -> &Arch {
&self.arch
}
pub fn os(&self) -> &Os {
&self.os
}
pub fn libc(&self) -> &Libc {
&self.libc
}
}
impl fmt::Display for PythonInstallationKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}-{}.{}.{}-{}-{}-{}",
self.implementation, self.major, self.minor, self.patch, self.os, self.arch, self.libc
)
}
}
impl FromStr for PythonInstallationKey {
type Err = PythonInstallationKeyError;
fn from_str(key: &str) -> Result<Self, Self::Err> {
let parts = key.split('-').collect::<Vec<_>>();
let [implementation, version, os, arch, libc] = parts.as_slice() else {
return Err(PythonInstallationKeyError::ParseError(
key.to_string(),
"not enough `-`-separated values".to_string(),
));
};
let implementation = LenientImplementationName::from(*implementation);
let os = Os::from_str(os).map_err(|err| {
PythonInstallationKeyError::ParseError(key.to_string(), format!("invalid OS: {err}"))
})?;
let arch = Arch::from_str(arch).map_err(|err| {
PythonInstallationKeyError::ParseError(
key.to_string(),
format!("invalid architecture: {err}"),
)
})?;
let libc = Libc::from_str(libc).map_err(|err| {
PythonInstallationKeyError::ParseError(key.to_string(), format!("invalid libc: {err}"))
})?;
let [major, minor, patch] = version
.splitn(3, '.')
.map(str::parse::<u8>)
.collect::<Result<Vec<_>, _>>()
.map_err(|err| {
PythonInstallationKeyError::ParseError(
key.to_string(),
format!("invalid Python version: {err}"),
)
})?[..]
else {
return Err(PythonInstallationKeyError::ParseError(
key.to_string(),
"invalid Python version: expected `<major>.<minor>.<patch>`".to_string(),
));
};
Ok(Self::new(
implementation,
major,
minor,
patch,
os,
arch,
libc,
))
}
}
impl PartialOrd for PythonInstallationKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PythonInstallationKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.to_string().cmp(&other.to_string())
}
}

View file

@ -0,0 +1,820 @@
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use configparser::ini::Ini;
use fs_err as fs;
use once_cell::sync::OnceCell;
use same_file::is_same_file;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::{trace, warn};
use cache_key::digest;
use install_wheel_rs::Layout;
use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, StringVersion};
use platform_tags::Platform;
use platform_tags::{Tags, TagsError};
use pypi_types::Scheme;
use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp};
use uv_fs::{write_atomic_sync, PythonExt, Simplified};
use crate::pointer_size::PointerSize;
use crate::{Prefix, PythonVersion, Target, VirtualEnvironment};
/// A Python executable and its associated platform markers.
#[derive(Debug, Clone)]
pub struct Interpreter {
platform: Platform,
markers: Box<MarkerEnvironment>,
scheme: Scheme,
virtualenv: Scheme,
sys_prefix: PathBuf,
sys_base_exec_prefix: PathBuf,
sys_base_prefix: PathBuf,
sys_base_executable: Option<PathBuf>,
sys_executable: PathBuf,
sys_path: Vec<PathBuf>,
stdlib: PathBuf,
tags: OnceCell<Tags>,
target: Option<Target>,
prefix: Option<Prefix>,
pointer_size: PointerSize,
gil_disabled: bool,
}
impl Interpreter {
/// Detect the interpreter info for the given Python executable.
pub fn query(executable: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
let info = InterpreterInfo::query_cached(executable.as_ref(), cache)?;
debug_assert!(
info.sys_executable.is_absolute(),
"`sys.executable` is not an absolute Python; Python installation is broken: {}",
info.sys_executable.display()
);
Ok(Self {
platform: info.platform,
markers: Box::new(info.markers),
scheme: info.scheme,
virtualenv: info.virtualenv,
sys_prefix: info.sys_prefix,
sys_base_exec_prefix: info.sys_base_exec_prefix,
pointer_size: info.pointer_size,
gil_disabled: info.gil_disabled,
sys_base_prefix: info.sys_base_prefix,
sys_base_executable: info.sys_base_executable,
sys_executable: info.sys_executable,
sys_path: info.sys_path,
stdlib: info.stdlib,
tags: OnceCell::new(),
target: None,
prefix: None,
})
}
// TODO(konstin): Find a better way mocking the fields
pub fn artificial(platform: Platform, markers: MarkerEnvironment) -> Self {
Self {
platform,
markers: Box::new(markers),
scheme: Scheme {
purelib: PathBuf::from("/dev/null"),
platlib: PathBuf::from("/dev/null"),
include: PathBuf::from("/dev/null"),
scripts: PathBuf::from("/dev/null"),
data: PathBuf::from("/dev/null"),
},
virtualenv: Scheme {
purelib: PathBuf::from("/dev/null"),
platlib: PathBuf::from("/dev/null"),
include: PathBuf::from("/dev/null"),
scripts: PathBuf::from("/dev/null"),
data: PathBuf::from("/dev/null"),
},
sys_prefix: PathBuf::from("/dev/null"),
sys_base_exec_prefix: PathBuf::from("/dev/null"),
sys_base_prefix: PathBuf::from("/dev/null"),
sys_base_executable: None,
sys_executable: PathBuf::from("/dev/null"),
sys_path: vec![],
stdlib: PathBuf::from("/dev/null"),
tags: OnceCell::new(),
target: None,
prefix: None,
pointer_size: PointerSize::_64,
gil_disabled: false,
}
}
/// Return a new [`Interpreter`] with the given virtual environment root.
#[must_use]
pub fn with_virtualenv(self, virtualenv: VirtualEnvironment) -> Self {
Self {
scheme: virtualenv.scheme,
sys_executable: virtualenv.executable,
sys_prefix: virtualenv.root,
target: None,
prefix: None,
..self
}
}
/// Return a new [`Interpreter`] to install into the given `--target` directory.
#[must_use]
pub fn with_target(self, target: Target) -> Self {
Self {
target: Some(target),
..self
}
}
/// Return a new [`Interpreter`] to install into the given `--prefix` directory.
#[must_use]
pub fn with_prefix(self, prefix: Prefix) -> Self {
Self {
prefix: Some(prefix),
..self
}
}
/// Returns the path to the Python virtual environment.
#[inline]
pub fn platform(&self) -> &Platform {
&self.platform
}
/// Returns the [`MarkerEnvironment`] for this Python executable.
#[inline]
pub const fn markers(&self) -> &MarkerEnvironment {
&self.markers
}
/// Returns the [`Tags`] for this Python executable.
pub fn tags(&self) -> Result<&Tags, TagsError> {
self.tags.get_or_try_init(|| {
Tags::from_env(
self.platform(),
self.python_tuple(),
self.implementation_name(),
self.implementation_tuple(),
self.gil_disabled,
)
})
}
/// Returns `true` if the environment is a PEP 405-compliant virtual environment.
///
/// See: <https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/utils/virtualenv.py#L14>
pub fn is_virtualenv(&self) -> bool {
// Maybe this should return `false` if it's a target?
self.sys_prefix != self.sys_base_prefix
}
/// Returns `true` if the environment is a `--target` environment.
pub fn is_target(&self) -> bool {
self.target.is_some()
}
/// Returns `true` if the environment is a `--prefix` environment.
pub fn is_prefix(&self) -> bool {
self.prefix.is_some()
}
/// Returns `Some` if the environment is externally managed, optionally including an error
/// message from the `EXTERNALLY-MANAGED` file.
///
/// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
// Per the spec, a virtual environment is never externally managed.
if self.is_virtualenv() {
return None;
}
// If we're installing into a target or prefix directory, it's never externally managed.
if self.is_target() || self.is_prefix() {
return None;
}
let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
return None;
};
let mut ini = Ini::new_cs();
ini.set_multiline(true);
let Ok(mut sections) = ini.read(contents) else {
// If a file exists but is not a valid INI file, we assume the environment is
// externally managed.
return Some(ExternallyManaged::default());
};
let Some(section) = sections.get_mut("externally-managed") else {
// If the file exists but does not contain an "externally-managed" section, we assume
// the environment is externally managed.
return Some(ExternallyManaged::default());
};
let Some(error) = section.remove("Error") else {
// If the file exists but does not contain an "Error" key, we assume the environment is
// externally managed.
return Some(ExternallyManaged::default());
};
Some(ExternallyManaged { error })
}
/// Returns the `python_full_version` marker corresponding to this Python version.
#[inline]
pub fn python_full_version(&self) -> &StringVersion {
self.markers.python_full_version()
}
/// Returns the full Python version.
#[inline]
pub fn python_version(&self) -> &Version {
&self.markers.python_full_version().version
}
/// Returns the full minor Python version.
#[inline]
pub fn python_minor_version(&self) -> Version {
Version::new(self.python_version().release().iter().take(2).copied())
}
/// Return the major version of this Python version.
pub fn python_major(&self) -> u8 {
let major = self.markers.python_full_version().version.release()[0];
u8::try_from(major).expect("invalid major version")
}
/// Return the minor version of this Python version.
pub fn python_minor(&self) -> u8 {
let minor = self.markers.python_full_version().version.release()[1];
u8::try_from(minor).expect("invalid minor version")
}
/// Return the patch version of this Python version.
pub fn python_patch(&self) -> u8 {
let minor = self.markers.python_full_version().version.release()[2];
u8::try_from(minor).expect("invalid patch version")
}
/// Returns the Python version as a simple tuple.
pub fn python_tuple(&self) -> (u8, u8) {
(self.python_major(), self.python_minor())
}
/// Return the major version of the implementation (e.g., `CPython` or `PyPy`).
pub fn implementation_major(&self) -> u8 {
let major = self.markers.implementation_version().version.release()[0];
u8::try_from(major).expect("invalid major version")
}
/// Return the minor version of the implementation (e.g., `CPython` or `PyPy`).
pub fn implementation_minor(&self) -> u8 {
let minor = self.markers.implementation_version().version.release()[1];
u8::try_from(minor).expect("invalid minor version")
}
/// Returns the implementation version as a simple tuple.
pub fn implementation_tuple(&self) -> (u8, u8) {
(self.implementation_major(), self.implementation_minor())
}
/// Returns the implementation name (e.g., `CPython` or `PyPy`).
pub fn implementation_name(&self) -> &str {
self.markers.implementation_name()
}
/// Return the `sys.base_exec_prefix` path for this Python interpreter.
pub fn sys_base_exec_prefix(&self) -> &Path {
&self.sys_base_exec_prefix
}
/// Return the `sys.base_prefix` path for this Python interpreter.
pub fn sys_base_prefix(&self) -> &Path {
&self.sys_base_prefix
}
/// Return the `sys.prefix` path for this Python interpreter.
pub fn sys_prefix(&self) -> &Path {
&self.sys_prefix
}
/// Return the `sys._base_executable` path for this Python interpreter. Some platforms do not
/// have this attribute, so it may be `None`.
pub fn sys_base_executable(&self) -> Option<&Path> {
self.sys_base_executable.as_deref()
}
/// Return the `sys.executable` path for this Python interpreter.
pub fn sys_executable(&self) -> &Path {
&self.sys_executable
}
/// Return the `sys.path` for this Python interpreter.
pub fn sys_path(&self) -> &Vec<PathBuf> {
&self.sys_path
}
/// Return the `stdlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
pub fn stdlib(&self) -> &Path {
&self.stdlib
}
/// Return the `purelib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
pub fn purelib(&self) -> &Path {
&self.scheme.purelib
}
/// Return the `platlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
pub fn platlib(&self) -> &Path {
&self.scheme.platlib
}
/// Return the `scripts` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
pub fn scripts(&self) -> &Path {
&self.scheme.scripts
}
/// Return the `data` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
pub fn data(&self) -> &Path {
&self.scheme.data
}
/// Return the `include` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
pub fn include(&self) -> &Path {
&self.scheme.include
}
/// Return the [`Scheme`] for a virtual environment created by this [`Interpreter`].
pub fn virtualenv(&self) -> &Scheme {
&self.virtualenv
}
/// Return the [`PointerSize`] of the Python interpreter (i.e., 32- vs. 64-bit).
pub fn pointer_size(&self) -> PointerSize {
self.pointer_size
}
/// Return whether this is a Python 3.13+ freethreading Python, as specified by the sysconfig var
/// `Py_GIL_DISABLED`.
///
/// freethreading Python is incompatible with earlier native modules, re-introducing
/// abiflags with a `t` flag. <https://peps.python.org/pep-0703/#build-configuration-changes>
pub fn gil_disabled(&self) -> bool {
self.gil_disabled
}
/// Return the `--target` directory for this interpreter, if any.
pub fn target(&self) -> Option<&Target> {
self.target.as_ref()
}
/// Return the `--prefix` directory for this interpreter, if any.
pub fn prefix(&self) -> Option<&Prefix> {
self.prefix.as_ref()
}
/// Return the [`Layout`] environment used to install wheels into this interpreter.
pub fn layout(&self) -> Layout {
Layout {
python_version: self.python_tuple(),
sys_executable: self.sys_executable().to_path_buf(),
os_name: self.markers.os_name().to_string(),
scheme: if let Some(target) = self.target.as_ref() {
target.scheme()
} else if let Some(prefix) = self.prefix.as_ref() {
prefix.scheme(&self.virtualenv)
} else {
Scheme {
purelib: self.purelib().to_path_buf(),
platlib: self.platlib().to_path_buf(),
scripts: self.scripts().to_path_buf(),
data: self.data().to_path_buf(),
include: if self.is_virtualenv() {
// If the interpreter is a venv, then the `include` directory has a different structure.
// See: https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/locations/_sysconfig.py#L172
self.sys_prefix.join("include").join("site").join(format!(
"python{}.{}",
self.python_major(),
self.python_minor()
))
} else {
self.include().to_path_buf()
},
}
},
}
}
/// Return an iterator over the `site-packages` directories inside the environment.
pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
let purelib = self.purelib();
let platlib = self.platlib();
std::iter::once(purelib).chain(
if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
None
} else {
Some(platlib)
},
)
}
/// Check if the interpreter matches the given Python version.
///
/// If a patch version is present, we will require an exact match.
/// Otherwise, just the major and minor version numbers need to match.
pub fn satisfies(&self, version: &PythonVersion) -> bool {
if version.patch().is_some() {
version.version() == self.python_version()
} else {
(version.major(), version.minor()) == self.python_tuple()
}
}
}
/// The `EXTERNALLY-MANAGED` file in a Python installation.
///
/// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
#[derive(Debug, Default, Clone)]
pub struct ExternallyManaged {
error: Option<String>,
}
impl ExternallyManaged {
/// Return the `EXTERNALLY-MANAGED` error message, if any.
pub fn into_error(self) -> Option<String> {
self.error
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Failed to query Python interpreter")]
Io(#[from] io::Error),
#[error("Python interpreter not found at `{0}`")]
NotFound(PathBuf),
#[error("Failed to query Python interpreter at `{path}`")]
SpawnFailed {
path: PathBuf,
#[source]
err: io::Error,
},
#[error("Querying Python at `{}` did not return the expected data\n{err}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---", path.display())]
UnexpectedResponse {
err: serde_json::Error,
stdout: String,
stderr: String,
path: PathBuf,
},
#[error("Querying Python at `{}` failed with exit status {code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---", path.display())]
StatusCode {
code: ExitStatus,
stdout: String,
stderr: String,
path: PathBuf,
},
#[error("Can't use Python at `{path}`")]
QueryScript {
#[source]
err: InterpreterInfoError,
path: PathBuf,
},
#[error("Failed to write to cache")]
Encode(#[from] rmp_serde::encode::Error),
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "result", rename_all = "lowercase")]
enum InterpreterInfoResult {
Error(InterpreterInfoError),
Success(Box<InterpreterInfo>),
}
#[derive(Debug, Error, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum InterpreterInfoError {
#[error("Could not detect a glibc or a musl libc (while running on Linux)")]
LibcNotFound,
#[error("Unknown operation system: `{operating_system}`")]
UnknownOperatingSystem { operating_system: String },
#[error("Python {python_version} is not supported. Please use Python 3.8 or newer.")]
UnsupportedPythonVersion { python_version: String },
#[error("Python executable does not support `-I` flag. Please use Python 3.8 or newer.")]
UnsupportedPython,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct InterpreterInfo {
platform: Platform,
markers: MarkerEnvironment,
scheme: Scheme,
virtualenv: Scheme,
sys_prefix: PathBuf,
sys_base_exec_prefix: PathBuf,
sys_base_prefix: PathBuf,
sys_base_executable: Option<PathBuf>,
sys_executable: PathBuf,
sys_path: Vec<PathBuf>,
stdlib: PathBuf,
pointer_size: PointerSize,
gil_disabled: bool,
}
impl InterpreterInfo {
/// Return the resolved [`InterpreterInfo`] for the given Python executable.
pub(crate) fn query(interpreter: &Path, cache: &Cache) -> Result<Self, Error> {
let tempdir = tempfile::tempdir_in(cache.root())?;
Self::setup_python_query_files(tempdir.path())?;
// Sanitize the path by (1) running under isolated mode (`-I`) to ignore any site packages
// modifications, and then (2) adding the path containing our query script to the front of
// `sys.path` so that we can import it.
let script = format!(
r#"import sys; sys.path = ["{}"] + sys.path; from python.get_interpreter_info import main; main()"#,
tempdir.path().escape_for_python()
);
let output = Command::new(interpreter)
.arg("-I")
.arg("-c")
.arg(script)
.output()
.map_err(|err| Error::SpawnFailed {
path: interpreter.to_path_buf(),
err,
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
// If the Python version is too old, we may not even be able to invoke the query script
if stderr.contains("Unknown option: -I") {
return Err(Error::QueryScript {
err: InterpreterInfoError::UnsupportedPython,
path: interpreter.to_path_buf(),
});
}
return Err(Error::StatusCode {
code: output.status,
stderr,
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
path: interpreter.to_path_buf(),
});
}
let result: InterpreterInfoResult =
serde_json::from_slice(&output.stdout).map_err(|err| {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
// If the Python version is too old, we may not even be able to invoke the query script
if stderr.contains("Unknown option: -I") {
Error::QueryScript {
err: InterpreterInfoError::UnsupportedPython,
path: interpreter.to_path_buf(),
}
} else {
Error::UnexpectedResponse {
err,
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
stderr,
path: interpreter.to_path_buf(),
}
}
})?;
match result {
InterpreterInfoResult::Error(err) => Err(Error::QueryScript {
err,
path: interpreter.to_path_buf(),
}),
InterpreterInfoResult::Success(data) => Ok(*data),
}
}
/// Duplicate the directory structure we have in `../python` into a tempdir, so we can run
/// the Python probing scripts with `python -m python.get_interpreter_info` from that tempdir.
fn setup_python_query_files(root: &Path) -> Result<(), Error> {
let python_dir = root.join("python");
fs_err::create_dir(&python_dir)?;
fs_err::write(
python_dir.join("get_interpreter_info.py"),
include_str!("../python/get_interpreter_info.py"),
)?;
fs_err::write(
python_dir.join("__init__.py"),
include_str!("../python/__init__.py"),
)?;
let packaging_dir = python_dir.join("packaging");
fs_err::create_dir(&packaging_dir)?;
fs_err::write(
packaging_dir.join("__init__.py"),
include_str!("../python/packaging/__init__.py"),
)?;
fs_err::write(
packaging_dir.join("_elffile.py"),
include_str!("../python/packaging/_elffile.py"),
)?;
fs_err::write(
packaging_dir.join("_manylinux.py"),
include_str!("../python/packaging/_manylinux.py"),
)?;
fs_err::write(
packaging_dir.join("_musllinux.py"),
include_str!("../python/packaging/_musllinux.py"),
)?;
Ok(())
}
/// A wrapper around [`markers::query_interpreter_info`] to cache the computed markers.
///
/// Running a Python script is (relatively) expensive, and the markers won't change
/// unless the Python executable changes, so we use the executable's last modified
/// time as a cache key.
pub(crate) fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
let cache_entry = cache.entry(
CacheBucket::Interpreter,
"",
// We use the absolute path for the cache entry to avoid cache collisions for relative paths
// but we do not want to query the executable with symbolic links resolved
format!("{}.msgpack", digest(&uv_fs::absolutize_path(executable)?)),
);
// We check the timestamp of the canonicalized executable to check if an underlying
// interpreter has been modified
let modified =
Timestamp::from_path(uv_fs::canonicalize_executable(executable).map_err(|err| {
if err.kind() == io::ErrorKind::NotFound {
Error::NotFound(executable.to_path_buf())
} else {
err.into()
}
})?)?;
// Read from the cache.
if cache
.freshness(&cache_entry, None)
.is_ok_and(Freshness::is_fresh)
{
if let Ok(data) = fs::read(cache_entry.path()) {
match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
Ok(cached) => {
if cached.timestamp == modified {
trace!(
"Cached interpreter info for Python {}, skipping probing: {}",
cached.data.markers.python_full_version(),
executable.user_display()
);
return Ok(cached.data);
}
trace!(
"Ignoring stale interpreter markers for: {}",
executable.user_display()
);
}
Err(err) => {
warn!(
"Broken interpreter cache entry at {}, removing: {err}",
cache_entry.path().user_display()
);
let _ = fs_err::remove_file(cache_entry.path());
}
}
}
}
// Otherwise, run the Python script.
trace!(
"Querying interpreter executable at {}",
executable.display()
);
let info = Self::query(executable, cache)?;
// If `executable` is a pyenv shim, a bash script that redirects to the activated
// python executable at another path, we're not allowed to cache the interpreter info.
if same_file::is_same_file(executable, &info.sys_executable).unwrap_or(false) {
fs::create_dir_all(cache_entry.dir())?;
write_atomic_sync(
cache_entry.path(),
rmp_serde::to_vec(&CachedByTimestamp {
timestamp: modified,
data: info.clone(),
})?,
)?;
}
Ok(info)
}
}
#[cfg(unix)]
#[cfg(test)]
mod tests {
use std::str::FromStr;
use fs_err as fs;
use indoc::{formatdoc, indoc};
use tempfile::tempdir;
use pep440_rs::Version;
use uv_cache::Cache;
use crate::Interpreter;
#[test]
fn test_cache_invalidation() {
let mock_dir = tempdir().unwrap();
let mocked_interpreter = mock_dir.path().join("python");
let json = indoc! {r##"
{
"result": "success",
"platform": {
"os": {
"name": "manylinux",
"major": 2,
"minor": 38
},
"arch": "x86_64"
},
"markers": {
"implementation_name": "cpython",
"implementation_version": "3.12.0",
"os_name": "posix",
"platform_machine": "x86_64",
"platform_python_implementation": "CPython",
"platform_release": "6.5.0-13-generic",
"platform_system": "Linux",
"platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023",
"python_full_version": "3.12.0",
"python_version": "3.12",
"sys_platform": "linux"
},
"sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0",
"sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0",
"sys_prefix": "/home/ferris/projects/uv/.venv",
"sys_executable": "/home/ferris/projects/uv/.venv/bin/python",
"sys_path": [
"/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12",
"/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
],
"stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12",
"scheme": {
"data": "/home/ferris/.pyenv/versions/3.12.0",
"include": "/home/ferris/.pyenv/versions/3.12.0/include",
"platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
"purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages",
"scripts": "/home/ferris/.pyenv/versions/3.12.0/bin"
},
"virtualenv": {
"data": "",
"include": "include",
"platlib": "lib/python3.12/site-packages",
"purelib": "lib/python3.12/site-packages",
"scripts": "bin"
},
"pointer_size": "64",
"gil_disabled": true
}
"##};
let cache = Cache::temp().unwrap().init().unwrap();
fs::write(
&mocked_interpreter,
formatdoc! {r##"
#!/bin/bash
echo '{json}'
"##},
)
.unwrap();
fs::set_permissions(
&mocked_interpreter,
std::os::unix::fs::PermissionsExt::from_mode(0o770),
)
.unwrap();
let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
assert_eq!(
interpreter.markers.python_version().version,
Version::from_str("3.12").unwrap()
);
fs::write(
&mocked_interpreter,
formatdoc! {r##"
#!/bin/bash
echo '{}'
"##, json.replace("3.12", "3.13")},
)
.unwrap();
let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap();
assert_eq!(
interpreter.markers.python_version().version,
Version::from_str("3.13").unwrap()
);
}
}

1870
crates/uv-python/src/lib.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,342 @@
use core::fmt;
use fs_err as fs;
use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use thiserror::Error;
use tracing::warn;
use uv_state::{StateBucket, StateStore};
use crate::downloads::Error as DownloadError;
use crate::implementation::{
Error as ImplementationError, ImplementationName, LenientImplementationName,
};
use crate::installation::{self, PythonInstallationKey};
use crate::platform::Error as PlatformError;
use crate::platform::{Arch, Libc, Os};
use crate::python_version::PythonVersion;
use crate::PythonRequest;
use uv_fs::{LockedFile, Simplified};
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
IO(#[from] io::Error),
#[error(transparent)]
Download(#[from] DownloadError),
#[error(transparent)]
PlatformError(#[from] PlatformError),
#[error(transparent)]
ImplementationError(#[from] ImplementationError),
#[error("Invalid python version: {0}")]
InvalidPythonVersion(String),
#[error(transparent)]
ExtractError(#[from] uv_extract::Error),
#[error("Failed to copy to: {0}", to.user_display())]
CopyError {
to: PathBuf,
#[source]
err: io::Error,
},
#[error("Failed to read Python installation directory: {0}", dir.user_display())]
ReadError {
dir: PathBuf,
#[source]
err: io::Error,
},
#[error("Failed to read managed Python directory name: {0}")]
NameError(String),
#[error(transparent)]
NameParseError(#[from] installation::PythonInstallationKeyError),
}
/// A collection of uv-managed Python installations installed on the current system.
#[derive(Debug, Clone)]
pub struct ManagedPythonInstallations {
/// The path to the top-level directory of the installed Python versions.
root: PathBuf,
}
impl ManagedPythonInstallations {
/// A directory for Python installations at `root`.
fn from_path(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
/// Lock the toolchains directory.
pub fn acquire_lock(&self) -> Result<LockedFile, Error> {
Ok(LockedFile::acquire(
self.root.join(".lock"),
self.root.user_display(),
)?)
}
/// Prefer, in order:
/// 1. The specific Python directory specified by the user, i.e., `UV_PYTHON_INSTALL_DIR`
/// 2. A directory in the system-appropriate user-level data directory, e.g., `~/.local/uv/python`
/// 3. A directory in the local data directory, e.g., `./.uv/python`
pub fn from_settings() -> Result<Self, Error> {
if let Some(install_dir) = std::env::var_os("UV_PYTHON_INSTALL_DIR") {
Ok(Self::from_path(install_dir))
} else {
Ok(Self::from_path(
StateStore::from_settings(None)?.bucket(StateBucket::ManagedPython),
))
}
}
/// Create a temporary Python installation directory.
pub fn temp() -> Result<Self, Error> {
Ok(Self::from_path(
StateStore::temp()?.bucket(StateBucket::ManagedPython),
))
}
/// Initialize the Python installation directory.
///
/// Ensures the directory is created.
pub fn init(self) -> Result<Self, Error> {
let root = &self.root;
// Support `toolchains` -> `python` migration transparently.
if !root.exists()
&& root
.parent()
.is_some_and(|parent| parent.join("toolchains").exists())
{
let deprecated = root.parent().unwrap().join("toolchains");
// Move the deprecated directory to the new location.
fs::rename(&deprecated, root)?;
// Create a link or junction to at the old location
uv_fs::replace_symlink(root, &deprecated)?;
} else {
fs::create_dir_all(root)?;
}
// Create the directory, if it doesn't exist.
fs::create_dir_all(root)?;
// Add a .gitignore.
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(root.join(".gitignore"))
{
Ok(mut file) => file.write_all(b"*")?,
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
Err(err) => return Err(err.into()),
}
Ok(self)
}
/// Iterate over each Python installation in this directory.
///
/// Pythons are sorted descending by name, such that we get deterministic
/// ordering across platforms. This also results in newer Python versions coming first,
/// but should not be relied on — instead the installations should be sorted later by
/// the parsed Python version.
pub fn find_all(
&self,
) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation>, Error> {
let dirs = match fs_err::read_dir(&self.root) {
Ok(installation_dirs) => {
// Collect sorted directory paths; `read_dir` is not stable across platforms
let directories: BTreeSet<_> = installation_dirs
.filter_map(|read_dir| match read_dir {
Ok(entry) => match entry.file_type() {
Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())),
Err(err) => Some(Err(err)),
},
Err(err) => Some(Err(err)),
})
.collect::<Result<_, std::io::Error>>()
.map_err(|err| Error::ReadError {
dir: self.root.clone(),
err,
})?;
directories
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => BTreeSet::default(),
Err(err) => {
return Err(Error::ReadError {
dir: self.root.clone(),
err,
})
}
};
Ok(dirs
.into_iter()
.filter_map(|path| {
ManagedPythonInstallation::new(path)
.inspect_err(|err| {
warn!("Ignoring malformed managed Python entry:\n {err}");
})
.ok()
})
.rev())
}
/// Iterate over Python installations that support the current platform.
pub fn find_matching_current_platform(
&self,
) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation>, Error> {
let platform_key = platform_key_from_env();
let iter = ManagedPythonInstallations::from_settings()?
.find_all()?
.filter(move |installation| {
installation
.path
.file_name()
.map(OsStr::to_string_lossy)
.is_some_and(|filename| filename.ends_with(&platform_key))
});
Ok(iter)
}
/// Iterate over managed Python installations that satisfy the requested version on this platform.
///
/// ## Errors
///
/// - The platform metadata cannot be read
/// - A directory for the installation cannot be read
pub fn find_version<'a>(
&self,
version: &'a PythonVersion,
) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation> + 'a, Error> {
Ok(self
.find_matching_current_platform()?
.filter(move |installation| {
installation
.path
.file_name()
.map(OsStr::to_string_lossy)
.is_some_and(|filename| filename.starts_with(&format!("cpython-{version}")))
}))
}
pub fn root(&self) -> &Path {
&self.root
}
}
static EXTERNALLY_MANAGED: &str = "[externally-managed]
Error=This Python installation is managed by uv and should not be modified.
";
/// A uv-managed Python installation on the current system..
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct ManagedPythonInstallation {
/// The path to the top-level directory of the installed Python.
path: PathBuf,
/// An install key for the Python version.
key: PythonInstallationKey,
}
impl ManagedPythonInstallation {
pub fn new(path: PathBuf) -> Result<Self, Error> {
let key = PythonInstallationKey::from_str(
path.file_name()
.ok_or(Error::NameError("name is empty".to_string()))?
.to_str()
.ok_or(Error::NameError("not a valid string".to_string()))?,
)?;
Ok(Self { path, key })
}
/// The path to this toolchain's Python executable.
pub fn executable(&self) -> PathBuf {
if cfg!(windows) {
self.path.join("install").join("python.exe")
} else if cfg!(unix) {
self.path.join("install").join("bin").join("python3")
} else {
unimplemented!("Only Windows and Unix systems are supported.")
}
}
/// The [`PythonVersion`] of the toolchain.
pub fn version(&self) -> PythonVersion {
self.key.version()
}
pub fn implementation(&self) -> &ImplementationName {
match self.key.implementation() {
LenientImplementationName::Known(implementation) => implementation,
LenientImplementationName::Unknown(_) => {
panic!("Managed Python installations should have a known implementation")
}
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn key(&self) -> &PythonInstallationKey {
&self.key
}
pub fn satisfies(&self, request: &PythonRequest) -> bool {
match request {
PythonRequest::File(path) => self.executable() == *path,
PythonRequest::Any => true,
PythonRequest::Directory(path) => self.path() == *path,
PythonRequest::ExecutableName(name) => self
.executable()
.file_name()
.is_some_and(|filename| filename.to_string_lossy() == *name),
PythonRequest::Implementation(implementation) => {
implementation == self.implementation()
}
PythonRequest::ImplementationVersion(implementation, version) => {
implementation == self.implementation() && version.matches_version(&self.version())
}
PythonRequest::Version(version) => version.matches_version(&self.version()),
PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
}
}
/// Ensure the environment is marked as externally managed with the
/// standard `EXTERNALLY-MANAGED` file.
pub fn ensure_externally_managed(&self) -> Result<(), Error> {
// Construct the path to the `stdlib` directory.
let stdlib = if cfg!(windows) {
self.path.join("install").join("Lib")
} else {
self.path
.join("install")
.join("lib")
.join(format!("python{}", self.key.version().python_version()))
};
let file = stdlib.join("EXTERNALLY-MANAGED");
fs_err::write(file, EXTERNALLY_MANAGED)?;
Ok(())
}
}
/// Generate a platform portion of a key from the environment.
fn platform_key_from_env() -> String {
let os = Os::from_env();
let arch = Arch::from_env();
let libc = Libc::from_env();
format!("{os}-{arch}-{libc}").to_lowercase()
}
impl fmt::Display for ManagedPythonInstallation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
self.path
.file_name()
.unwrap_or(self.path.as_os_str())
.to_string_lossy()
)
}
}

View file

@ -0,0 +1,195 @@
use std::fmt::Display;
use std::ops::Deref;
use std::{fmt, str::FromStr};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Unknown operating system: {0}")]
UnknownOs(String),
#[error("Unknown architecture: {0}")]
UnknownArch(String),
#[error("Unknown libc environment: {0}")]
UnknownLibc(String),
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub struct Arch(pub(crate) target_lexicon::Architecture);
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub struct Os(pub(crate) target_lexicon::OperatingSystem);
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum Libc {
Some(target_lexicon::Environment),
None,
}
impl Libc {
pub(crate) fn from_env() -> Self {
match std::env::consts::OS {
// TODO(zanieb): On Linux, we use the uv target host to determine the libc variant
// but we should only use this as a fallback and should instead inspect the
// machine's `/bin/sh` (or similar).
"linux" => Self::Some(target_lexicon::Environment::Gnu),
"windows" | "macos" => Self::None,
// Use `None` on platforms without explicit support.
_ => Self::None,
}
}
}
impl FromStr for Libc {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)),
"musl" => Ok(Self::Some(target_lexicon::Environment::Musl)),
"none" => Ok(Self::None),
_ => Err(Error::UnknownLibc(s.to_string())),
}
}
}
impl Os {
pub fn from_env() -> Self {
Self(target_lexicon::HOST.operating_system)
}
}
impl Arch {
pub fn from_env() -> Self {
Self(target_lexicon::HOST.architecture)
}
}
impl Display for Libc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Some(env) => write!(f, "{env}"),
Self::None => write!(f, "none"),
}
}
}
impl Display for Os {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &**self {
target_lexicon::OperatingSystem::Darwin => write!(f, "macos"),
inner => write!(f, "{inner}"),
}
}
}
impl Display for Arch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &**self {
target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686) => {
write!(f, "x86")
}
inner => write!(f, "{inner}"),
}
}
}
impl FromStr for Os {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let inner = match s {
"macos" => target_lexicon::OperatingSystem::Darwin,
_ => target_lexicon::OperatingSystem::from_str(s)
.map_err(|()| Error::UnknownOs(s.to_string()))?,
};
if matches!(inner, target_lexicon::OperatingSystem::Unknown) {
return Err(Error::UnknownOs(s.to_string()));
}
Ok(Self(inner))
}
}
impl FromStr for Arch {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let inner = match s {
// Allow users to specify "x86" as a shorthand for the "i686" variant, they should not need
// to specify the exact architecture and this variant is what we have downloads for.
"x86" => target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686),
_ => target_lexicon::Architecture::from_str(s)
.map_err(|()| Error::UnknownArch(s.to_string()))?,
};
if matches!(inner, target_lexicon::Architecture::Unknown) {
return Err(Error::UnknownArch(s.to_string()));
}
Ok(Self(inner))
}
}
impl Deref for Arch {
type Target = target_lexicon::Architecture;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Deref for Os {
type Target = target_lexicon::OperatingSystem;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&platform_tags::Arch> for Arch {
fn from(value: &platform_tags::Arch) -> Self {
match value {
platform_tags::Arch::Aarch64 => Self(target_lexicon::Architecture::Aarch64(
target_lexicon::Aarch64Architecture::Aarch64,
)),
platform_tags::Arch::Armv6L => Self(target_lexicon::Architecture::Arm(
target_lexicon::ArmArchitecture::Armv6,
)),
platform_tags::Arch::Armv7L => Self(target_lexicon::Architecture::Arm(
target_lexicon::ArmArchitecture::Armv7,
)),
platform_tags::Arch::S390X => Self(target_lexicon::Architecture::S390x),
platform_tags::Arch::Powerpc64 => Self(target_lexicon::Architecture::Powerpc64),
platform_tags::Arch::Powerpc64Le => Self(target_lexicon::Architecture::Powerpc64le),
platform_tags::Arch::X86 => Self(target_lexicon::Architecture::X86_32(
target_lexicon::X86_32Architecture::I686,
)),
platform_tags::Arch::X86_64 => Self(target_lexicon::Architecture::X86_64),
}
}
}
impl From<&platform_tags::Os> for Libc {
fn from(value: &platform_tags::Os) -> Self {
match value {
platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu),
platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl),
_ => Self::None,
}
}
}
impl From<&platform_tags::Os> for Os {
fn from(value: &platform_tags::Os) -> Self {
match value {
platform_tags::Os::Dragonfly { .. } => Self(target_lexicon::OperatingSystem::Dragonfly),
platform_tags::Os::FreeBsd { .. } => Self(target_lexicon::OperatingSystem::Freebsd),
platform_tags::Os::Haiku { .. } => Self(target_lexicon::OperatingSystem::Haiku),
platform_tags::Os::Illumos { .. } => Self(target_lexicon::OperatingSystem::Illumos),
platform_tags::Os::Macos { .. } => Self(target_lexicon::OperatingSystem::Darwin),
platform_tags::Os::Manylinux { .. } | platform_tags::Os::Musllinux { .. } => {
Self(target_lexicon::OperatingSystem::Linux)
}
platform_tags::Os::NetBsd { .. } => Self(target_lexicon::OperatingSystem::Netbsd),
platform_tags::Os::OpenBsd { .. } => Self(target_lexicon::OperatingSystem::Openbsd),
platform_tags::Os::Windows => Self(target_lexicon::OperatingSystem::Windows),
}
}
}

View file

@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum PointerSize {
/// 32-bit architecture.
#[serde(rename = "32")]
_32,
/// 64-bit architecture.
#[serde(rename = "64")]
_64,
}
impl PointerSize {
pub const fn is_32(self) -> bool {
matches!(self, Self::_32)
}
pub const fn is_64(self) -> bool {
matches!(self, Self::_64)
}
}

View file

@ -0,0 +1,43 @@
use std::path::{Path, PathBuf};
use pypi_types::Scheme;
/// A `--prefix` directory into which packages can be installed, separate from a virtual environment
/// or system Python interpreter.
#[derive(Debug, Clone)]
pub struct Prefix(PathBuf);
impl Prefix {
/// Return the [`Scheme`] for the `--prefix` directory.
pub fn scheme(&self, virtualenv: &Scheme) -> Scheme {
Scheme {
purelib: self.0.join(&virtualenv.purelib),
platlib: self.0.join(&virtualenv.platlib),
scripts: self.0.join(&virtualenv.scripts),
data: self.0.join(&virtualenv.data),
include: self.0.join(&virtualenv.include),
}
}
/// Return an iterator over the `site-packages` directories inside the environment.
pub fn site_packages(&self, virtualenv: &Scheme) -> impl Iterator<Item = PathBuf> {
std::iter::once(self.0.join(&virtualenv.purelib))
}
/// Initialize the `--prefix` directory.
pub fn init(&self) -> std::io::Result<()> {
fs_err::create_dir_all(&self.0)?;
Ok(())
}
/// Return the path to the `--prefix` directory.
pub fn root(&self) -> &Path {
&self.0
}
}
impl From<PathBuf> for Prefix {
fn from(path: PathBuf) -> Self {
Self(path)
}
}

View file

@ -0,0 +1,94 @@
use std::io;
use std::path::PathBuf;
use std::process::{Command, ExitStatus};
use once_cell::sync::Lazy;
use regex::Regex;
use thiserror::Error;
use tracing::info_span;
#[derive(Debug, Clone)]
pub(crate) struct PyListPath {
pub(crate) major: u8,
pub(crate) minor: u8,
pub(crate) executable_path: PathBuf,
}
/// An error was encountered when using the `py` launcher on Windows.
#[derive(Error, Debug)]
pub enum Error {
#[error("{message} with {exit_code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")]
StatusCode {
message: String,
exit_code: ExitStatus,
stdout: String,
stderr: String,
},
#[error("Failed to run `py --list-paths` to find Python installations.")]
Io(#[source] io::Error),
#[error("The `py` launcher could not be found.")]
NotFound,
}
/// ```text
/// -V:3.12 C:\Users\Ferris\AppData\Local\Programs\Python\Python312\python.exe
/// -V:3.8 C:\Users\Ferris\AppData\Local\Programs\Python\Python38\python.exe
/// ```
static PY_LIST_PATHS: Lazy<Regex> = Lazy::new(|| {
// Without the `R` flag, paths have trailing \r
Regex::new(r"(?mR)^ -(?:V:)?(\d).(\d+)-?(?:arm)?\d*\s*\*?\s*(.*)$").unwrap()
});
/// Use the `py` launcher to find installed Python versions.
///
/// Calls `py --list-paths`.
pub(crate) fn py_list_paths() -> Result<Vec<PyListPath>, Error> {
// konstin: The command takes 8ms on my machine.
let output = info_span!("py_list_paths")
.in_scope(|| Command::new("py").arg("--list-paths").output())
.map_err(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
Error::NotFound
} else {
Error::Io(err)
}
})?;
// `py` sometimes prints "Installed Pythons found by py Launcher for Windows" to stderr which we ignore.
if !output.status.success() {
return Err(Error::StatusCode {
message: format!(
"Running `py --list-paths` failed with status {}",
output.status
),
exit_code: output.status,
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
});
}
// Find the first python of the version we want in the list
let stdout = String::from_utf8(output.stdout).map_err(|err| Error::StatusCode {
message: format!("The stdout of `py --list-paths` isn't UTF-8 encoded: {err}"),
exit_code: output.status,
stdout: String::from_utf8_lossy(err.as_bytes()).trim().to_string(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
})?;
Ok(PY_LIST_PATHS
.captures_iter(&stdout)
.filter_map(|captures| {
let (_, [major, minor, path]) = captures.extract();
if let (Some(major), Some(minor)) = (major.parse::<u8>().ok(), minor.parse::<u8>().ok())
{
Some(PyListPath {
major,
minor,
executable_path: PathBuf::from(path),
})
} else {
None
}
})
.collect())
}

View file

@ -0,0 +1,210 @@
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::str::FromStr;
use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, StringVersion};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PythonVersion(StringVersion);
impl Deref for PythonVersion {
type Target = StringVersion;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromStr for PythonVersion {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let version = StringVersion::from_str(s)
.map_err(|err| format!("Python version `{s}` could not be parsed: {err}"))?;
if version.is_dev() {
return Err(format!("Python version `{s}` is a development release"));
}
if version.is_local() {
return Err(format!("Python version `{s}` is a local version"));
}
if version.epoch() != 0 {
return Err(format!("Python version `{s}` has a non-zero epoch"));
}
if version.version < Version::new([3, 7]) {
return Err(format!("Python version `{s}` must be >= 3.7"));
}
if version.version >= Version::new([4, 0]) {
return Err(format!("Python version `{s}` must be < 4.0"));
}
Ok(Self(version))
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for PythonVersion {
fn schema_name() -> String {
String::from("PythonVersion")
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
string: Some(Box::new(schemars::schema::StringValidation {
pattern: Some(r"^3\.\d+(\.\d+)?$".to_string()),
..schemars::schema::StringValidation::default()
})),
metadata: Some(Box::new(schemars::schema::Metadata {
description: Some("A Python version specifier, e.g. `3.7` or `3.8.0`.".to_string()),
..schemars::schema::Metadata::default()
})),
..schemars::schema::SchemaObject::default()
}
.into()
}
}
impl<'de> serde::Deserialize<'de> for PythonVersion {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
PythonVersion::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl Display for PythonVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl PythonVersion {
/// Return a [`MarkerEnvironment`] compatible with the given [`PythonVersion`], based on
/// a base [`MarkerEnvironment`].
///
/// The returned [`MarkerEnvironment`] will preserve the base environment's platform markers,
/// but override its Python version markers.
pub fn markers(self, base: &MarkerEnvironment) -> MarkerEnvironment {
let mut markers = base.clone();
// Ex) `implementation_version == "3.12.0"`
if markers.implementation_name() == "cpython" {
let python_full_version = self.python_full_version();
markers = markers.with_implementation_version(StringVersion {
// Retain the verbatim representation, provided by the user.
string: self.0.to_string(),
version: python_full_version,
});
}
// Ex) `python_full_version == "3.12.0"`
let python_full_version = self.python_full_version();
markers = markers.with_python_full_version(StringVersion {
// Retain the verbatim representation, provided by the user.
string: self.0.to_string(),
version: python_full_version,
});
// Ex) `python_version == "3.12"`
let python_version = self.python_version();
markers = markers.with_python_version(StringVersion {
string: python_version.to_string(),
version: python_version,
});
markers
}
/// Return the `python_version` marker corresponding to this Python version.
///
/// This should include exactly a major and minor version, but no patch version.
///
/// Ex) `python_version == "3.12"`
pub fn python_version(&self) -> Version {
let major = self.release().first().copied().unwrap_or(0);
let minor = self.release().get(1).copied().unwrap_or(0);
Version::new([major, minor])
}
/// Return the `python_full_version` marker corresponding to this Python version.
///
/// This should include exactly a major, minor, and patch version (even if it's zero), along
/// with any pre-release or post-release information.
///
/// Ex) `python_full_version == "3.12.0b1"`
pub fn python_full_version(&self) -> Version {
let major = self.release().first().copied().unwrap_or(0);
let minor = self.release().get(1).copied().unwrap_or(0);
let patch = self.release().get(2).copied().unwrap_or(0);
Version::new([major, minor, patch])
.with_pre(self.0.pre())
.with_post(self.0.post())
}
/// Return the full parsed Python version.
pub fn version(&self) -> &Version {
&self.0.version
}
/// Return the major version of this Python version.
pub fn major(&self) -> u8 {
u8::try_from(self.0.release().first().copied().unwrap_or(0)).expect("invalid major version")
}
/// Return the minor version of this Python version.
pub fn minor(&self) -> u8 {
u8::try_from(self.0.release().get(1).copied().unwrap_or(0)).expect("invalid minor version")
}
/// Return the patch version of this Python version, if set.
pub fn patch(&self) -> Option<u8> {
self.0
.release()
.get(2)
.copied()
.map(|patch| u8::try_from(patch).expect("invalid patch version"))
}
/// Returns a copy of the Python version without the patch version
#[must_use]
pub fn without_patch(&self) -> Self {
Self::from_str(format!("{}.{}", self.major(), self.minor()).as_str())
.expect("dropping a patch should always be valid")
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use pep440_rs::{PreRelease, PreReleaseKind, Version};
use crate::PythonVersion;
#[test]
fn python_markers() {
let version = PythonVersion::from_str("3.11.0").expect("valid python version");
assert_eq!(version.python_version(), Version::new([3, 11]));
assert_eq!(version.python_version().to_string(), "3.11");
assert_eq!(version.python_full_version(), Version::new([3, 11, 0]));
assert_eq!(version.python_full_version().to_string(), "3.11.0");
let version = PythonVersion::from_str("3.11").expect("valid python version");
assert_eq!(version.python_version(), Version::new([3, 11]));
assert_eq!(version.python_version().to_string(), "3.11");
assert_eq!(version.python_full_version(), Version::new([3, 11, 0]));
assert_eq!(version.python_full_version().to_string(), "3.11.0");
let version = PythonVersion::from_str("3.11.8a1").expect("valid python version");
assert_eq!(version.python_version(), Version::new([3, 11]));
assert_eq!(version.python_version().to_string(), "3.11");
assert_eq!(
version.python_full_version(),
Version::new([3, 11, 8]).with_pre(Some(PreRelease {
kind: PreReleaseKind::Alpha,
number: 1
}))
);
assert_eq!(version.python_full_version().to_string(), "3.11.8a1");
}
}

View file

@ -0,0 +1,43 @@
use std::path::{Path, PathBuf};
use pypi_types::Scheme;
/// A `--target` directory into which packages can be installed, separate from a virtual environment
/// or system Python interpreter.
#[derive(Debug, Clone)]
pub struct Target(PathBuf);
impl Target {
/// Return the [`Scheme`] for the `--target` directory.
pub fn scheme(&self) -> Scheme {
Scheme {
purelib: self.0.clone(),
platlib: self.0.clone(),
scripts: self.0.join("bin"),
data: self.0.clone(),
include: self.0.join("include"),
}
}
/// Return an iterator over the `site-packages` directories inside the environment.
pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
std::iter::once(self.0.as_path())
}
/// Initialize the `--target` directory.
pub fn init(&self) -> std::io::Result<()> {
fs_err::create_dir_all(&self.0)?;
Ok(())
}
/// Return the path to the `--target` directory.
pub fn root(&self) -> &Path {
&self.0
}
}
impl From<PathBuf> for Target {
fn from(path: PathBuf) -> Self {
Self(path)
}
}

View file

@ -0,0 +1,67 @@
use fs_err as fs;
use std::{io, path::PathBuf};
use tracing::debug;
use crate::PythonRequest;
/// Read [`PythonRequest`]s from a version file, if present.
///
/// Prefers `.python-versions` then `.python-version`.
/// If only one Python version is desired, use [`request_from_version_files`] which prefers the `.python-version` file.
pub async fn requests_from_version_file() -> Result<Option<Vec<PythonRequest>>, io::Error> {
if let Some(versions) = read_versions_file().await? {
Ok(Some(
versions
.into_iter()
.map(|version| PythonRequest::parse(&version))
.collect(),
))
} else if let Some(version) = read_version_file().await? {
Ok(Some(vec![PythonRequest::parse(&version)]))
} else {
Ok(None)
}
}
/// Read a [`PythonRequest`] from a version file, if present.
///
/// Prefers `.python-version` then the first entry of `.python-versions`.
/// If multiple Python versions are desired, use [`requests_from_version_files`] instead.
pub async fn request_from_version_file() -> Result<Option<PythonRequest>, io::Error> {
if let Some(version) = read_version_file().await? {
Ok(Some(PythonRequest::parse(&version)))
} else if let Some(versions) = read_versions_file().await? {
Ok(versions
.into_iter()
.next()
.inspect(|_| debug!("Using the first version from `.python-versions`"))
.map(|version| PythonRequest::parse(&version)))
} else {
Ok(None)
}
}
async fn read_versions_file() -> Result<Option<Vec<String>>, io::Error> {
if !PathBuf::from(".python-versions").try_exists()? {
return Ok(None);
}
debug!("Reading requests from `.python-versions`");
let lines: Vec<String> = fs::tokio::read_to_string(".python-versions")
.await?
.lines()
.map(ToString::to_string)
.collect();
Ok(Some(lines))
}
async fn read_version_file() -> Result<Option<String>, io::Error> {
if !PathBuf::from(".python-version").try_exists()? {
return Ok(None);
}
debug!("Reading requests from `.python-version`");
Ok(fs::tokio::read_to_string(".python-version")
.await?
.lines()
.next()
.map(ToString::to_string))
}

View file

@ -0,0 +1,172 @@
use std::{
env, io,
path::{Path, PathBuf},
};
use fs_err as fs;
use pypi_types::Scheme;
use thiserror::Error;
/// The layout of a virtual environment.
#[derive(Debug)]
pub struct VirtualEnvironment {
/// The absolute path to the root of the virtualenv, e.g., `/path/to/.venv`.
pub root: PathBuf,
/// The path to the Python interpreter inside the virtualenv, e.g., `.venv/bin/python`
/// (Unix, Python 3.11).
pub executable: PathBuf,
/// The [`Scheme`] paths for the virtualenv, as returned by (e.g.) `sysconfig.get_paths()`.
pub scheme: Scheme,
}
/// A parsed `pyvenv.cfg`
#[derive(Debug, Clone)]
pub struct PyVenvConfiguration {
/// If the `virtualenv` package was used to create the virtual environment.
pub(crate) virtualenv: bool,
/// If the `uv` package was used to create the virtual environment.
pub(crate) uv: bool,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Broken virtualenv `{0}`: `pyvenv.cfg` is missing")]
MissingPyVenvCfg(PathBuf),
#[error("Broken virtualenv `{0}`: `pyvenv.cfg` could not be parsed")]
ParsePyVenvCfg(PathBuf, #[source] io::Error),
#[error(transparent)]
IO(#[from] io::Error),
}
/// Locate an active virtual environment by inspecting environment variables.
///
/// Supports `VIRTUAL_ENV`.
pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
if let Some(dir) = env::var_os("VIRTUAL_ENV").filter(|value| !value.is_empty()) {
return Some(PathBuf::from(dir));
}
None
}
/// Locate an active conda environment by inspecting environment variables.
///
/// Supports `CONDA_PREFIX`.
pub(crate) fn conda_prefix_from_env() -> Option<PathBuf> {
if let Some(dir) = env::var_os("CONDA_PREFIX").filter(|value| !value.is_empty()) {
return Some(PathBuf::from(dir));
}
None
}
/// Locate a virtual environment by searching the file system.
///
/// Searches for a `.venv` directory in the current or any parent directory. If the current
/// directory is itself a virtual environment (or a subdirectory of a virtual environment), the
/// containing virtual environment is returned.
pub(crate) fn virtualenv_from_working_dir() -> Result<Option<PathBuf>, Error> {
let current_dir = crate::current_dir()?;
for dir in current_dir.ancestors() {
// If we're _within_ a virtualenv, return it.
if dir.join("pyvenv.cfg").is_file() {
return Ok(Some(dir.to_path_buf()));
}
// Otherwise, search for a `.venv` directory.
let dot_venv = dir.join(".venv");
if dot_venv.is_dir() {
if !dot_venv.join("pyvenv.cfg").is_file() {
return Err(Error::MissingPyVenvCfg(dot_venv));
}
return Ok(Some(dot_venv));
}
}
Ok(None)
}
/// Returns the path to the `python` executable inside a virtual environment.
pub(crate) fn virtualenv_python_executable(venv: impl AsRef<Path>) -> PathBuf {
let venv = venv.as_ref();
if cfg!(windows) {
// Search for `python.exe` in the `Scripts` directory.
let default_executable = venv.join("Scripts").join("python.exe");
if default_executable.exists() {
return default_executable;
}
// Apparently, Python installed via msys2 on Windows _might_ produce a POSIX-like layout.
// See: https://github.com/PyO3/maturin/issues/1108
let executable = venv.join("bin").join("python.exe");
if executable.exists() {
return executable;
}
// Fallback for Conda environments.
let executable = venv.join("python.exe");
if executable.exists() {
return executable;
}
// If none of these exist, return the standard location
default_executable
} else {
// Check for both `python3` over `python`, preferring the more specific one
let default_executable = venv.join("bin").join("python3");
if default_executable.exists() {
return default_executable;
}
let executable = venv.join("bin").join("python");
if executable.exists() {
return executable;
}
// If none of these exist, return the standard location
default_executable
}
}
impl PyVenvConfiguration {
/// Parse a `pyvenv.cfg` file into a [`PyVenvConfiguration`].
pub fn parse(cfg: impl AsRef<Path>) -> Result<Self, Error> {
let mut virtualenv = false;
let mut uv = false;
// Per https://snarky.ca/how-virtual-environments-work/, the `pyvenv.cfg` file is not a
// valid INI file, and is instead expected to be parsed by partitioning each line on the
// first equals sign.
let content = fs::read_to_string(&cfg)
.map_err(|err| Error::ParsePyVenvCfg(cfg.as_ref().to_path_buf(), err))?;
for line in content.lines() {
let Some((key, _value)) = line.split_once('=') else {
continue;
};
match key.trim() {
"virtualenv" => {
virtualenv = true;
}
"uv" => {
uv = true;
}
_ => {}
}
}
Ok(Self { virtualenv, uv })
}
/// Returns true if the virtual environment was created with the `virtualenv` package.
pub fn is_virtualenv(&self) -> bool {
self.virtualenv
}
/// Returns true if the virtual environment was created with the `uv` package.
pub fn is_uv(&self) -> bool {
self.uv
}
}

View file

@ -0,0 +1,128 @@
#!/usr/bin/env python3.12
"""
Generate static Rust code from Python version download metadata.
Generates the `downloads.inc` file from the `downloads.inc.mustache` template.
Usage:
uv run --isolated --with chevron-blue -- crates/uv-python/template-download-metadata.py
"""
import sys
import logging
import argparse
import json
import subprocess
from pathlib import Path
CRATE_ROOT = Path(__file__).parent
WORKSPACE_ROOT = CRATE_ROOT.parent.parent
VERSION_METADATA = CRATE_ROOT / "download-metadata.json"
TEMPLATE = CRATE_ROOT / "src" / "downloads.inc.mustache"
TARGET = TEMPLATE.with_suffix("")
try:
import chevron_blue
except ImportError:
print(
"missing requirement `chevron-blue`",
file=sys.stderr,
)
exit(1)
def prepare_name(name: str) -> str:
match name:
case "cpython":
return "CPython"
case _:
raise ValueError(f"Unknown implementation name: {name}")
def prepare_libc(libc: str) -> str | None:
if libc == "none":
return None
else:
return libc.title()
def prepare_arch(arch: str) -> str:
match arch:
# Special constructors
case "i686":
return "X86_32(target_lexicon::X86_32Architecture::I686)"
case "aarch64":
return "Aarch64(target_lexicon::Aarch64Architecture::Aarch64)"
case "armv7":
return "Arm(target_lexicon::ArmArchitecture::Armv7)"
case _:
return arch.capitalize()
def prepare_value(value: dict) -> dict:
value["os"] = value["os"].title()
value["arch"] = prepare_arch(value["arch"])
value["name"] = prepare_name(value["name"])
value["libc"] = prepare_libc(value["libc"])
return value
def main():
debug = logging.getLogger().getEffectiveLevel() <= logging.DEBUG
data = {}
data["generated_with"] = Path(__file__).relative_to(WORKSPACE_ROOT)
data["generated_from"] = TEMPLATE.relative_to(WORKSPACE_ROOT)
data["versions"] = [
{"key": key, "value": prepare_value(value)}
for key, value in json.loads(VERSION_METADATA.read_text()).items()
]
# Render the template
logging.info(f"Rendering `{TEMPLATE.name}`...")
output = chevron_blue.render(
template=TEMPLATE.read_text(), data=data, no_escape=True, warn=debug
)
# Update the file
logging.info(f"Updating `{TARGET}`...")
TARGET.write_text(output)
subprocess.check_call(
["rustfmt", str(TARGET)],
stderr=subprocess.STDOUT,
stdout=sys.stderr if debug else subprocess.DEVNULL,
)
logging.info("Done!")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generates Rust code for Python version metadata.",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable debug logging",
)
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Disable logging",
)
args = parser.parse_args()
if args.quiet:
log_level = logging.CRITICAL
elif args.verbose:
log_level = logging.DEBUG
else:
log_level = logging.INFO
logging.basicConfig(level=log_level, format="%(message)s")
main()