mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 20:31:12 +00:00
Add GraalPy support (#5141)
<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:
- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->
## Summary
Currently, `uv` refuses to install anything on GraalPy. This is
currently blocking GraalPy testing with cibuildwheel, since manylinux
includes both `uv` and `graalpy` (but doesn't test with `uv`), whereas
cibuildwheel defaults to `uv`. See e.g.
2750618295
where it gives
```
+ python -m build /project/sample_proj --wheel --outdir=/tmp/cibuildwheel/built_wheel --installer=uv
* Creating isolated environment: venv+uv...
* Using external uv from /usr/local/bin/uv
* Installing packages in isolated environment:
- setuptools >= 40.8.0
> /usr/local/bin/uv pip install "setuptools >= 40.8.0"
< error: Unknown implementation: `graalpy`
```
## Test Plan
I simply based the GraalPy support on PyPy and added some small tests.
I'm open to discussing how to test this. GraalPy is available for
manylinux images and with setup-python, so we should be able to add
tests against it to the CI. I locally confirmed by installing `uv` into
a GraalPy venv and then trying things like `uv pip install Pillow` and
testing those extensions.
This commit is contained in:
parent
54bca18184
commit
24a0268675
6 changed files with 397 additions and 8 deletions
131
.github/workflows/ci.yml
vendored
131
.github/workflows/ci.yml
vendored
|
|
@ -692,6 +692,137 @@ jobs:
|
|||
run: |
|
||||
.\uv.exe pip install anyio
|
||||
|
||||
integration-test-graalpy-linux:
|
||||
needs: build-binary-linux
|
||||
name: "integration test | graalpy on ubuntu"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "graalpy24.0"
|
||||
|
||||
- name: "Download binary"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: uv-linux-${{ github.sha }}
|
||||
|
||||
- name: "Prepare binary"
|
||||
run: chmod +x ./uv
|
||||
|
||||
- name: Graalpy info
|
||||
run: |
|
||||
which graalpy
|
||||
echo "GRAAL_PYTHONHOME=$(graalpy -c 'print(__graalpython__.home)')" >> $GITHUB_ENV
|
||||
|
||||
- name: "Create a virtual environment"
|
||||
run: |
|
||||
./uv venv -p $(which graalpy)
|
||||
|
||||
- name: "Check for executables"
|
||||
run: |
|
||||
check_in_bin() {
|
||||
local executable_name=$1
|
||||
local bin_path=".venv/bin"
|
||||
|
||||
if [[ -x "$bin_path/$executable_name" ]]; then
|
||||
return 0
|
||||
else
|
||||
echo "Executable '$executable_name' not found in folder '$bin_path'."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
executables=("graalpy" "python3" "python")
|
||||
|
||||
all_found=true
|
||||
for executable_name in "${executables[@]}"; do
|
||||
check_in_bin "$executable_name" "$folder_path"
|
||||
result=$?
|
||||
|
||||
if [[ $result -ne 0 ]]; then
|
||||
all_found=false
|
||||
fi
|
||||
done
|
||||
|
||||
if ! $all_found; then
|
||||
echo "One or more expected executables were not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: "Check version"
|
||||
run: |
|
||||
.venv/bin/graalpy --version
|
||||
.venv/bin/python3 --version
|
||||
.venv/bin/python --version
|
||||
|
||||
- name: "Check install"
|
||||
run: |
|
||||
./uv pip install anyio
|
||||
|
||||
integration-test-graalpy-windows:
|
||||
needs: build-binary-windows
|
||||
name: "integration test | graalpy on windows"
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: timfel/setup-python@fc9bcb4a04f5b1ea7d678c2ca7ea1c479a2468d7
|
||||
with:
|
||||
python-version: "graalpy24.0"
|
||||
|
||||
- name: "Download binary"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: uv-windows-${{ github.sha }}
|
||||
|
||||
- name: Graalpy info
|
||||
run: Get-Command graalpy
|
||||
|
||||
- name: "Create a virtual environment"
|
||||
run: |
|
||||
$graalpy = (Get-Command graalpy).source
|
||||
.\uv.exe venv -p $graalpy
|
||||
|
||||
- name: "Check for executables"
|
||||
shell: python
|
||||
run: |
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def binary_exist(binary):
|
||||
binaries_path = Path(".venv\\Scripts")
|
||||
if (binaries_path / binary).exists():
|
||||
return True
|
||||
print(f"Executable '{binary}' not found in folder '{binaries_path}'.")
|
||||
|
||||
all_found = True
|
||||
expected_binaries = [
|
||||
"graalpy.exe",
|
||||
"python.exe",
|
||||
"python3.exe",
|
||||
]
|
||||
for binary in expected_binaries:
|
||||
if not binary_exist(binary):
|
||||
all_found = False
|
||||
|
||||
if not all_found:
|
||||
print("One or more expected executables were not found.")
|
||||
sys.exit(1)
|
||||
|
||||
- name: "Check version"
|
||||
run: |
|
||||
& .venv\Scripts\graalpy.exe --version
|
||||
& .venv\Scripts\python3.exe --version
|
||||
& .venv\Scripts\python.exe --version
|
||||
|
||||
- name: "Check install"
|
||||
env:
|
||||
# Avoid debug build stack overflows.
|
||||
UV_STACK_SIZE: 2000000 # 2 megabyte, double the default on windows
|
||||
run: |
|
||||
.\uv.exe pip install anyio
|
||||
|
||||
integration-test-github-actions:
|
||||
needs: build-binary-linux
|
||||
name: "integration test | github actions"
|
||||
|
|
|
|||
|
|
@ -298,6 +298,7 @@ impl std::fmt::Display for Tags {
|
|||
enum Implementation {
|
||||
CPython { gil_disabled: bool },
|
||||
PyPy,
|
||||
GraalPy,
|
||||
Pyston,
|
||||
}
|
||||
|
||||
|
|
@ -310,6 +311,8 @@ impl Implementation {
|
|||
Self::CPython { .. } => format!("cp{}{}", python_version.0, python_version.1),
|
||||
// Ex) `pp39`
|
||||
Self::PyPy => format!("pp{}{}", python_version.0, python_version.1),
|
||||
// Ex) `graalpy310`
|
||||
Self::GraalPy => format!("graalpy{}{}", python_version.0, python_version.1),
|
||||
// Ex) `pt38``
|
||||
Self::Pyston => format!("pt{}{}", python_version.0, python_version.1),
|
||||
}
|
||||
|
|
@ -342,6 +345,16 @@ impl Implementation {
|
|||
implementation_version.0,
|
||||
implementation_version.1
|
||||
),
|
||||
// Ex) `graalpy310_graalpy240_310_native
|
||||
Self::GraalPy => format!(
|
||||
"graalpy{}{}_graalpy{}{}_{}{}_native",
|
||||
python_version.0,
|
||||
python_version.1,
|
||||
implementation_version.0,
|
||||
implementation_version.1,
|
||||
python_version.0,
|
||||
python_version.1
|
||||
),
|
||||
// Ex) `pyston38-pyston_23`
|
||||
Self::Pyston => format!(
|
||||
"pyston{}{}-pyston_{}{}",
|
||||
|
|
@ -361,6 +374,7 @@ impl Implementation {
|
|||
// Known and supported implementations.
|
||||
"cpython" => Ok(Self::CPython { gil_disabled }),
|
||||
"pypy" => Ok(Self::PyPy),
|
||||
"graalpy" => Ok(Self::GraalPy),
|
||||
"pyston" => Ok(Self::Pyston),
|
||||
// Known but unsupported implementations.
|
||||
"python" => Err(TagsError::UnsupportedImplementation(name.to_string())),
|
||||
|
|
|
|||
|
|
@ -1710,6 +1710,14 @@ mod tests {
|
|||
PythonRequest::parse("pp"),
|
||||
PythonRequest::Implementation(ImplementationName::PyPy)
|
||||
);
|
||||
assert_eq!(
|
||||
PythonRequest::parse("graalpy"),
|
||||
PythonRequest::Implementation(ImplementationName::GraalPy)
|
||||
);
|
||||
assert_eq!(
|
||||
PythonRequest::parse("gp"),
|
||||
PythonRequest::Implementation(ImplementationName::GraalPy)
|
||||
);
|
||||
assert_eq!(
|
||||
PythonRequest::parse("cp"),
|
||||
PythonRequest::Implementation(ImplementationName::CPython)
|
||||
|
|
@ -1728,6 +1736,20 @@ mod tests {
|
|||
VersionRequest::from_str("3.10").unwrap()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
PythonRequest::parse("graalpy3.10"),
|
||||
PythonRequest::ImplementationVersion(
|
||||
ImplementationName::GraalPy,
|
||||
VersionRequest::from_str("3.10").unwrap()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
PythonRequest::parse("gp310"),
|
||||
PythonRequest::ImplementationVersion(
|
||||
ImplementationName::GraalPy,
|
||||
VersionRequest::from_str("3.10").unwrap()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
PythonRequest::parse("cp38"),
|
||||
PythonRequest::ImplementationVersion(
|
||||
|
|
@ -1749,6 +1771,20 @@ mod tests {
|
|||
VersionRequest::from_str("3.10").unwrap()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
PythonRequest::parse("graalpy@3.10"),
|
||||
PythonRequest::ImplementationVersion(
|
||||
ImplementationName::GraalPy,
|
||||
VersionRequest::from_str("3.10").unwrap()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
PythonRequest::parse("graalpy310"),
|
||||
PythonRequest::ImplementationVersion(
|
||||
ImplementationName::GraalPy,
|
||||
VersionRequest::from_str("3.10").unwrap()
|
||||
)
|
||||
);
|
||||
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert_eq!(
|
||||
|
|
@ -1819,6 +1855,18 @@ mod tests {
|
|||
.to_canonical_string(),
|
||||
"pypy@3.10"
|
||||
);
|
||||
assert_eq!(
|
||||
PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
|
||||
"graalpy"
|
||||
);
|
||||
assert_eq!(
|
||||
PythonRequest::ImplementationVersion(
|
||||
ImplementationName::GraalPy,
|
||||
VersionRequest::from_str("3.10").unwrap()
|
||||
)
|
||||
.to_canonical_string(),
|
||||
"graalpy@3.10"
|
||||
);
|
||||
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ pub enum ImplementationName {
|
|||
#[default]
|
||||
CPython,
|
||||
PyPy,
|
||||
GraalPy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
|
||||
|
|
@ -25,13 +26,14 @@ pub enum LenientImplementationName {
|
|||
|
||||
impl ImplementationName {
|
||||
pub(crate) fn possible_names() -> impl Iterator<Item = &'static str> {
|
||||
["cpython", "pypy", "cp", "pp"].into_iter()
|
||||
["cpython", "pypy", "graalpy", "cp", "pp", "gp"].into_iter()
|
||||
}
|
||||
|
||||
pub fn pretty(self) -> &'static str {
|
||||
match self {
|
||||
Self::CPython => "CPython",
|
||||
Self::PyPy => "PyPy",
|
||||
Self::GraalPy => "GraalPy",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +52,7 @@ impl From<&ImplementationName> for &'static str {
|
|||
match v {
|
||||
ImplementationName::CPython => "cpython",
|
||||
ImplementationName::PyPy => "pypy",
|
||||
ImplementationName::GraalPy => "graalpy",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -73,6 +76,7 @@ impl FromStr for ImplementationName {
|
|||
match s.to_ascii_lowercase().as_str() {
|
||||
"cpython" | "cp" => Ok(Self::CPython),
|
||||
"pypy" | "pp" => Ok(Self::PyPy),
|
||||
"graalpy" | "gp" => Ok(Self::GraalPy),
|
||||
_ => Err(Error::UnknownImplementation(s.to_string())),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1896,4 +1896,160 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_python_graalpy() -> Result<()> {
|
||||
let mut context = TestContext::new()?;
|
||||
|
||||
context.add_python_interpreters(&[(
|
||||
true,
|
||||
ImplementationName::GraalPy,
|
||||
"graalpy",
|
||||
"3.10.0",
|
||||
)])?;
|
||||
let result = context.run(|| {
|
||||
find_python_installation(
|
||||
&PythonRequest::Any,
|
||||
EnvironmentPreference::Any,
|
||||
PythonPreference::OnlySystem,
|
||||
&context.cache,
|
||||
)
|
||||
})?;
|
||||
assert!(
|
||||
matches!(result, Err(PythonNotFound { .. })),
|
||||
"We should not the graalpy interpreter if not named `python` or requested; got {result:?}"
|
||||
);
|
||||
|
||||
// But we should find it
|
||||
context.reset_search_path();
|
||||
context.add_python_interpreters(&[(
|
||||
true,
|
||||
ImplementationName::GraalPy,
|
||||
"python",
|
||||
"3.10.1",
|
||||
)])?;
|
||||
let python = context.run(|| {
|
||||
find_python_installation(
|
||||
&PythonRequest::Any,
|
||||
EnvironmentPreference::Any,
|
||||
PythonPreference::OnlySystem,
|
||||
&context.cache,
|
||||
)
|
||||
})??;
|
||||
assert_eq!(
|
||||
python.interpreter().python_full_version().to_string(),
|
||||
"3.10.1",
|
||||
"We should find the graalpy interpreter if it's the only one"
|
||||
);
|
||||
|
||||
let python = context.run(|| {
|
||||
find_python_installation(
|
||||
&PythonRequest::parse("graalpy"),
|
||||
EnvironmentPreference::Any,
|
||||
PythonPreference::OnlySystem,
|
||||
&context.cache,
|
||||
)
|
||||
})??;
|
||||
assert_eq!(
|
||||
python.interpreter().python_full_version().to_string(),
|
||||
"3.10.1",
|
||||
"We should find the graalpy interpreter if it's requested"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_python_graalpy_request_ignores_cpython() -> Result<()> {
|
||||
let mut context = TestContext::new()?;
|
||||
context.add_python_interpreters(&[
|
||||
(true, ImplementationName::CPython, "python", "3.10.0"),
|
||||
(true, ImplementationName::GraalPy, "graalpy", "3.10.1"),
|
||||
])?;
|
||||
|
||||
let python = context.run(|| {
|
||||
find_python_installation(
|
||||
&PythonRequest::parse("graalpy"),
|
||||
EnvironmentPreference::Any,
|
||||
PythonPreference::OnlySystem,
|
||||
&context.cache,
|
||||
)
|
||||
})??;
|
||||
assert_eq!(
|
||||
python.interpreter().python_full_version().to_string(),
|
||||
"3.10.1",
|
||||
"We should skip the CPython interpreter"
|
||||
);
|
||||
|
||||
let python = context.run(|| {
|
||||
find_python_installation(
|
||||
&PythonRequest::Any,
|
||||
EnvironmentPreference::Any,
|
||||
PythonPreference::OnlySystem,
|
||||
&context.cache,
|
||||
)
|
||||
})??;
|
||||
assert_eq!(
|
||||
python.interpreter().python_full_version().to_string(),
|
||||
"3.10.0",
|
||||
"We should take the first interpreter without a specific request"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_python_graalpy_prefers_executable_with_implementation_name() -> Result<()> {
|
||||
let mut context = TestContext::new()?;
|
||||
|
||||
// We should prefer `graalpy` executables over `python` executables in the same directory
|
||||
// even if they are both graalpy
|
||||
TestContext::create_mock_interpreter(
|
||||
&context.tempdir.join("python"),
|
||||
&PythonVersion::from_str("3.10.0").unwrap(),
|
||||
ImplementationName::GraalPy,
|
||||
true,
|
||||
)?;
|
||||
TestContext::create_mock_interpreter(
|
||||
&context.tempdir.join("graalpy"),
|
||||
&PythonVersion::from_str("3.10.1").unwrap(),
|
||||
ImplementationName::GraalPy,
|
||||
true,
|
||||
)?;
|
||||
context.add_to_search_path(context.tempdir.to_path_buf());
|
||||
|
||||
let python = context.run(|| {
|
||||
find_python_installation(
|
||||
&PythonRequest::parse("graalpy@3.10"),
|
||||
EnvironmentPreference::Any,
|
||||
PythonPreference::OnlySystem,
|
||||
&context.cache,
|
||||
)
|
||||
})??;
|
||||
assert_eq!(
|
||||
python.interpreter().python_full_version().to_string(),
|
||||
"3.10.1",
|
||||
);
|
||||
|
||||
// But `python` executables earlier in the search path will take precedence
|
||||
context.reset_search_path();
|
||||
context.add_python_interpreters(&[
|
||||
(true, ImplementationName::GraalPy, "python", "3.10.2"),
|
||||
(true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
|
||||
])?;
|
||||
let python = context.run(|| {
|
||||
find_python_installation(
|
||||
&PythonRequest::parse("graalpy@3.10"),
|
||||
EnvironmentPreference::Any,
|
||||
PythonPreference::OnlySystem,
|
||||
&context.cache,
|
||||
)
|
||||
})??;
|
||||
assert_eq!(
|
||||
python.interpreter().python_full_version().to_string(),
|
||||
"3.10.2",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,10 @@ pub(crate) fn create(
|
|||
)?;
|
||||
uv_fs::replace_symlink("python", scripts.join("pypy"))?;
|
||||
}
|
||||
|
||||
if interpreter.markers().implementation_name() == "graalpy" {
|
||||
uv_fs::replace_symlink("python", scripts.join("graalpy"))?;
|
||||
}
|
||||
}
|
||||
|
||||
// No symlinking on Windows, at least not on a regular non-dev non-admin Windows install.
|
||||
|
|
@ -189,6 +193,23 @@ pub(crate) fn create(
|
|||
&scripts,
|
||||
python_home,
|
||||
)?;
|
||||
|
||||
if interpreter.markers().implementation_name() == "graalpy" {
|
||||
copy_launcher_windows(
|
||||
WindowsExecutable::GraalPy,
|
||||
interpreter,
|
||||
&base_python,
|
||||
&scripts,
|
||||
python_home,
|
||||
)?;
|
||||
copy_launcher_windows(
|
||||
WindowsExecutable::PythonMajor,
|
||||
interpreter,
|
||||
&base_python,
|
||||
&scripts,
|
||||
python_home,
|
||||
)?;
|
||||
} else {
|
||||
copy_launcher_windows(
|
||||
WindowsExecutable::Pythonw,
|
||||
interpreter,
|
||||
|
|
@ -196,6 +217,7 @@ pub(crate) fn create(
|
|||
&scripts,
|
||||
python_home,
|
||||
)?;
|
||||
}
|
||||
|
||||
if interpreter.markers().implementation_name() == "pypy" {
|
||||
copy_launcher_windows(
|
||||
|
|
@ -319,6 +341,16 @@ pub(crate) fn create(
|
|||
pyvenv_cfg_data.push(("prompt".to_string(), prompt));
|
||||
}
|
||||
|
||||
if cfg!(windows) && interpreter.markers().implementation_name() == "graalpy" {
|
||||
pyvenv_cfg_data.push((
|
||||
"venvlauncher_command".to_string(),
|
||||
python_home
|
||||
.join("graalpy.exe")
|
||||
.simplified_display()
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?);
|
||||
write_cfg(&mut pyvenv_cfg, &pyvenv_cfg_data)?;
|
||||
drop(pyvenv_cfg);
|
||||
|
|
@ -380,6 +412,8 @@ enum WindowsExecutable {
|
|||
PyPyw,
|
||||
/// The `pypy3.<minor>w.exe` executable.
|
||||
PyPyMajorMinorw,
|
||||
// The `graalpy.exe` executable
|
||||
GraalPy,
|
||||
}
|
||||
|
||||
impl WindowsExecutable {
|
||||
|
|
@ -417,6 +451,7 @@ impl WindowsExecutable {
|
|||
interpreter.python_minor()
|
||||
)
|
||||
}
|
||||
WindowsExecutable::GraalPy => String::from("graalpy.exe"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -434,6 +469,7 @@ impl WindowsExecutable {
|
|||
WindowsExecutable::PyPyMajorMinor => "venvlauncher.exe",
|
||||
WindowsExecutable::PyPyw => "venvwlauncher.exe",
|
||||
WindowsExecutable::PyPyMajorMinorw => "venvwlauncher.exe",
|
||||
WindowsExecutable::GraalPy => "venvlauncher.exe",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue