Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve log when distutils is missing #10713

Merged
merged 7 commits into from
Jan 20, 2025
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
45 changes: 44 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -680,13 +680,56 @@ jobs:
echo "$CONDA_PREFIX"
./uv pip install anyio

integration-test-deadsnakes-39-linux:
timeout-minutes: 5
needs: build-binary-linux
name: "integration test | deadsnakes python3.9 on ubuntu"
runs-on: ubuntu-latest
steps:
- name: "Install python3.9"
run: |
sudo add-apt-repository ppa:deadsnakes
sudo apt-get update
sudo apt-get install python3.9

- name: "Download binary"
uses: actions/download-artifact@v4
with:
name: uv-linux-${{ github.sha }}

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

- name: "Create a virtual environment"
run: |
./uv venv -p 3.9 --python-preference only-system

- name: "Check version"
run: |
.venv/bin/python --version

- name: "Check install missing distutils"
run: |
./uv pip install -v anyio 2>&1 | tee log.txt || true
# We should report that distutils is missing
grep 'Python installation is missing `distutils`' log.txt

- name: "Install distutils"
run: |
sudo apt-get install python3.9-distutils

- name: "Check install"
run: |
./uv pip install -v anyio
# Now the install should succeed

integration-test-free-threaded-linux:
timeout-minutes: 5
needs: build-binary-linux
name: "integration test | free-threaded on linux"
runs-on: ubuntu-latest
steps:
- name: "install python3.13-nogil"
- name: "Install python3.13-nogil"
run: |
sudo add-apt-repository ppa:deadsnakes
sudo apt-get update
Expand Down
43 changes: 32 additions & 11 deletions crates/uv-python/python/get_interpreter_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def expand_path(path: str) -> str:
}


def get_scheme():
def get_scheme(use_sysconfig_scheme: bool):
"""Return the Scheme for the current interpreter.

The paths returned should be absolute.
Expand Down Expand Up @@ -401,15 +401,7 @@ def get_distutils_scheme():
"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:
if use_sysconfig_scheme:
Copy link
Member Author

Choose a reason for hiding this comment

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

I needed to move this out of get_scheme or get_virtualenv would fail on distutils import.

return get_sysconfig_scheme()
else:
return get_distutils_scheme()
Expand Down Expand Up @@ -571,6 +563,35 @@ def main() -> None:
elif os_and_arch["os"]["name"] == "musllinux":
manylinux_compatible = True


# 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_scheme = bool(
getattr(sysconfig, "_PIP_USE_SYSCONFIG", sys.version_info >= (3, 10))
)

# If we're not using sysconfig, make sure distutils is available.
if not use_sysconfig_scheme:
try:
import distutils.dist
except ImportError:
# We require distutils, but it's not installed; this is fairly
# common in, e.g., deadsnakes where distutils is packaged
# separately from Python.
print(
json.dumps(
{
"result": "error",
"kind": "missing_required_distutils",
"python_major": sys.version_info[0],
"python_minor": sys.version_info[1],
}
)
)
sys.exit(0)

interpreter_info = {
"result": "success",
"markers": markers,
Expand All @@ -585,7 +606,7 @@ def main() -> None:
# "/install" as the prefix. With `sysconfig` patching, we rewrite the prefix to match the actual installation
# location. So in newer versions, we also write a dedicated flag to indicate standalone builds.
"standalone": sysconfig.get_config_var("prefix") == "/install" or bool(sysconfig.get_config_var("PYTHON_BUILD_STANDALONE")),
"scheme": get_scheme(),
"scheme": get_scheme(use_sysconfig_scheme),
"virtualenv": get_virtualenv(),
"platform": os_and_arch,
"manylinux_compatible": manylinux_compatible,
Expand Down
10 changes: 8 additions & 2 deletions crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -720,15 +720,21 @@ impl Error {
InterpreterError::Encode(_)
| InterpreterError::Io(_)
| InterpreterError::SpawnFailed { .. } => true,
InterpreterError::QueryScript { path, .. }
| InterpreterError::UnexpectedResponse { path, .. }
InterpreterError::UnexpectedResponse { path, .. }
| InterpreterError::StatusCode { path, .. } => {
debug!(
"Skipping bad interpreter at {} from {source}: {err}",
path.display()
);
false
}
InterpreterError::QueryScript { path, err } => {
debug!(
"Skipping bad interpreter at {} from {source}: {err}",
path.display()
);
false
}
InterpreterError::NotFound(path) => {
// If the interpreter is from an active, valid virtual environment, we should
// fail because it's broken
Expand Down
5 changes: 5 additions & 0 deletions crates/uv-python/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,11 @@ pub enum InterpreterInfoError {
UnsupportedPythonVersion { python_version: String },
#[error("Python executable does not support `-I` flag. Please use Python 3.8 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`.")]
MissingRequiredDistutils {
python_major: usize,
python_minor: usize,
},
}

#[derive(Debug, Deserialize, Serialize, Clone)]
Expand Down
Loading