Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/test-system.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,54 @@ jobs:
- name: "Validate global Python install"
run: python scripts/check_system_python.py --uv ./uv

system-test-python-36:
timeout-minutes: 10
name: "python3.6 on debian buster"
runs-on: ubuntu-latest
container: python:3.6-buster
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: "Download binary"
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: uv-linux-musl-${{ inputs.sha }}

- name: "Prepare binary"
run: chmod +x ./uv

- name: "Print Python path"
run: echo $(which python3)

- name: "Validate global Python install"
run: python3 scripts/check_system_python.py --uv ./uv

system-test-python-37:
timeout-minutes: 10
name: "python3.7 on debian buster"
runs-on: ubuntu-latest
container: python:3.7-buster
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: "Download binary"
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: uv-linux-musl-${{ inputs.sha }}

- name: "Prepare binary"
run: chmod +x ./uv

- name: "Print Python path"
run: echo $(which python3)

- name: "Validate global Python install"
run: python3 scripts/check_system_python.py --uv ./uv

# Currently failing, see https://github.com/astral-sh/uv/issues/13811
# system-test-opensuse:
# timeout-minutes: 5
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/python/get_interpreter_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ def get_operating_system_and_architecture():
version = None
architecture = version_arch

if sys.version_info < (3, 7):
if sys.version_info < (3, 6):
print(
json.dumps(
{
Expand Down
6 changes: 6 additions & 0 deletions crates/uv-python/python/packaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ This directory contains vendored [pypa/packaging](https://github.com/pypa/packag
[cc938f984bbbe43c5734b9656c9837ab3a28191f](https://github.com/pypa/packaging/tree/cc938f984bbbe43c5734b9656c9837ab3a28191f/src/packaging).

The files are licensed under BSD-2-Clause OR Apache-2.0.

## Patches

The following patches have been applied:

- [python36-support.patch](./python36-support.patch)
6 changes: 2 additions & 4 deletions crates/uv-python/python/packaging/_elffile.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
"""

from __future__ import annotations

import enum
import os
import struct
Expand Down Expand Up @@ -88,11 +86,11 @@ def __init__(self, f: IO[bytes]) -> None:
except struct.error as e:
raise ELFInvalid("unable to parse machine and section information") from e

def _read(self, fmt: str) -> tuple[int, ...]:
def _read(self, fmt: str) -> "tuple[int, ...]":
return struct.unpack(fmt, self._f.read(struct.calcsize(fmt)))

@property
def interpreter(self) -> str | None:
def interpreter(self) -> "str | None":
"""
The path recorded in the ``PT_INTERP`` section header.
"""
Expand Down
16 changes: 7 additions & 9 deletions crates/uv-python/python/packaging/_manylinux.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import annotations

import collections
import contextlib
import functools
Expand All @@ -19,7 +17,7 @@
# `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]:
def _parse_elf(path: str) -> "Generator[ELFFile | None, None, None]":
try:
with open(path, "rb") as f:
yield ELFFile(f)
Expand Down Expand Up @@ -74,15 +72,15 @@ def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
# 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)
_LAST_GLIBC_MINOR: "dict[int, int]" = collections.defaultdict(lambda: 50)


class _GLibCVersion(NamedTuple):
major: int
minor: int


def _glibc_version_string_confstr() -> str | None:
def _glibc_version_string_confstr() -> "str | None":
"""
Primary implementation of glibc_version_string using os.confstr.
"""
Expand All @@ -92,7 +90,7 @@ def _glibc_version_string_confstr() -> str | None:
# 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")
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):
Expand All @@ -101,7 +99,7 @@ def _glibc_version_string_confstr() -> str | None:
return version


def _glibc_version_string_ctypes() -> str | None:
def _glibc_version_string_ctypes() -> "str | None":
"""
Fallback implementation of glibc_version_string using ctypes.
"""
Expand Down Expand Up @@ -145,7 +143,7 @@ def _glibc_version_string_ctypes() -> str | None:
return version_str


def _glibc_version_string() -> str | None:
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()

Expand Down Expand Up @@ -203,7 +201,7 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
return True


_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = {
_LEGACY_MANYLINUX_MAP: "dict[_GLibCVersion, str]" = {
# CentOS 7 w/ glibc 2.17 (PEP 599)
_GLibCVersion(2, 17): "manylinux2014",
# CentOS 6 w/ glibc 2.12 (PEP 571)
Expand Down
6 changes: 2 additions & 4 deletions crates/uv-python/python/packaging/_musllinux.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
linked against musl, and what musl version is used.
"""

from __future__ import annotations

import functools
import re
import subprocess
Expand All @@ -20,7 +18,7 @@ class _MuslVersion(NamedTuple):
minor: int


def _parse_musl_version(output: str) -> _MuslVersion | None:
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
Expand All @@ -31,7 +29,7 @@ def _parse_musl_version(output: str) -> _MuslVersion | None:


@functools.lru_cache()
def _get_musl_version(executable: str) -> _MuslVersion | None:
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
Expand Down
139 changes: 139 additions & 0 deletions crates/uv-python/python/packaging/python36-support.patch
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by comment without a full review: IMO it's not useful to check this file in, because it carries a risk of diverging from the actual patches to the .py files. I would simply rely on git history and the README (and maybe rephrase the README to emphasize that these are patched versions of the vendored files) and manually cherry-pick the patch when revendoring.

Alternatively, leave this file in, but don't commit the changes to the .py files in source control, and instead apply this file at build time.

Copy link
Copy Markdown
Member Author

@zanieb zanieb Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm I'm quite partial to checking it in so it's easy to apply in the future and very clear what changed. I'm pretty hesitant to deal with applying it at build time.

These files rarely change and we're a small team. I think optimizing for strictly preventing drift here is a bit silly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll write a script to perform the vendoring.

Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
Subject: [PATCH] python36-support

Remove `from __future__ import annotations` and quote PEP 604 (`X | Y`) union
annotations and PEP 585 (`dict[K, V]`) lowercase generics so these vendored
modules remain compatible with Python 3.6.

---
diff --git a/crates/uv-python/python/packaging/_elffile.py b/crates/uv-python/python/packaging/_elffile.py
index 8dc7fb32a..f1907a595 100644
--- a/crates/uv-python/python/packaging/_elffile.py
+++ b/crates/uv-python/python/packaging/_elffile.py
@@ -8,8 +8,6 @@ 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
@@ -88,11 +86,11 @@ class ELFFile:
except struct.error as e:
raise ELFInvalid("unable to parse machine and section information") from e

- def _read(self, fmt: str) -> tuple[int, ...]:
+ def _read(self, fmt: str) -> "tuple[int, ...]":
return struct.unpack(fmt, self._f.read(struct.calcsize(fmt)))

@property
- def interpreter(self) -> str | None:
+ def interpreter(self) -> "str | None":
"""
The path recorded in the ``PT_INTERP`` section header.
"""
diff --git a/crates/uv-python/python/packaging/_manylinux.py b/crates/uv-python/python/packaging/_manylinux.py
index 7b52a5581..d3c871aab 100644
--- a/crates/uv-python/python/packaging/_manylinux.py
+++ b/crates/uv-python/python/packaging/_manylinux.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import collections
import contextlib
import functools
@@ -19,7 +17,7 @@ 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]:
+def _parse_elf(path: str) -> "Generator[ELFFile | None, None, None]":
try:
with open(path, "rb") as f:
yield ELFFile(f)
@@ -74,7 +72,7 @@ def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
# 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)
+_LAST_GLIBC_MINOR: "dict[int, int]" = collections.defaultdict(lambda: 50)


class _GLibCVersion(NamedTuple):
@@ -82,7 +80,7 @@ class _GLibCVersion(NamedTuple):
minor: int


-def _glibc_version_string_confstr() -> str | None:
+def _glibc_version_string_confstr() -> "str | None":
"""
Primary implementation of glibc_version_string using os.confstr.
"""
@@ -92,7 +90,7 @@ def _glibc_version_string_confstr() -> str | None:
# 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")
+ 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):
@@ -101,7 +99,7 @@ def _glibc_version_string_confstr() -> str | None:
return version


-def _glibc_version_string_ctypes() -> str | None:
+def _glibc_version_string_ctypes() -> "str | None":
"""
Fallback implementation of glibc_version_string using ctypes.
"""
@@ -145,7 +143,7 @@ def _glibc_version_string_ctypes() -> str | None:
return version_str


-def _glibc_version_string() -> str | None:
+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()

@@ -203,7 +201,7 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
return True


-_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = {
+_LEGACY_MANYLINUX_MAP: "dict[_GLibCVersion, str]" = {
# CentOS 7 w/ glibc 2.17 (PEP 599)
_GLibCVersion(2, 17): "manylinux2014",
# CentOS 6 w/ glibc 2.12 (PEP 571)
diff --git a/crates/uv-python/python/packaging/_musllinux.py b/crates/uv-python/python/packaging/_musllinux.py
index b4ca23804..40a72f05b 100644
--- a/crates/uv-python/python/packaging/_musllinux.py
+++ b/crates/uv-python/python/packaging/_musllinux.py
@@ -4,8 +4,6 @@ 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
@@ -20,7 +18,7 @@ class _MuslVersion(NamedTuple):
minor: int


-def _parse_musl_version(output: str) -> _MuslVersion | None:
+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
@@ -31,7 +29,7 @@ def _parse_musl_version(output: str) -> _MuslVersion | None:


@functools.lru_cache()
-def _get_musl_version(executable: str) -> _MuslVersion | None:
+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
14 changes: 7 additions & 7 deletions crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ fn find_all_minor(
let minor = captures["minor"].parse().ok();
if let Some(minor) = minor {
// Optimization: Skip generally unsupported Python versions without querying.
if minor < 7 {
if minor < 6 {
return false;
}
// Optimization 2: Skip excluded Python (minor) versions without querying.
Expand Down Expand Up @@ -2751,23 +2751,23 @@ impl VersionRequest {
}
}
Self::MajorMinor(major, minor, _) => {
if (*major, *minor) < (3, 7) {
if (*major, *minor) < (3, 6) {
return Err(format!(
"Python <3.7 is not supported but {major}.{minor} was requested."
"Python <3.6 is not supported but {major}.{minor} was requested."
));
}
}
Self::MajorMinorPatch(major, minor, patch, _) => {
if (*major, *minor) < (3, 7) {
if (*major, *minor) < (3, 6) {
return Err(format!(
"Python <3.7 is not supported but {major}.{minor}.{patch} was requested."
"Python <3.6 is not supported but {major}.{minor}.{patch} was requested."
));
}
}
Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
if (*major, *minor) < (3, 7) {
if (*major, *minor) < (3, 6) {
return Err(format!(
"Python <3.7 is not supported but {major}.{minor}{prerelease} was requested."
"Python <3.6 is not supported but {major}.{minor}{prerelease} was requested."
));
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/uv-python/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -905,9 +905,9 @@ pub enum InterpreterInfoError {
BrokenMacVer,
#[error("Unknown operating system: `{operating_system}`")]
UnknownOperatingSystem { operating_system: String },
#[error("Python {python_version} is not supported. Please use Python 3.8 or newer.")]
#[error("Python {python_version} is not supported. Please use Python 3.6 or newer.")]
UnsupportedPythonVersion { python_version: String },
#[error("Python executable does not support `-I` flag. Please use Python 3.8 or newer.")]
#[error("Python executable does not support `-I` flag. Please use Python 3.6 or newer.")]
UnsupportedPython,
#[error(
"Python installation is missing `distutils`, which is required for packaging on older Python versions. Your system may package it separately, e.g., as `python{python_major}-distutils` or `python{python_major}.{python_minor}-distutils`."
Expand Down
Loading
Loading