mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Add support for embedded Python on Windows (#3161)
## Summary
References:
-
cad550030a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py (L58-L68)
- https://github.com/pypa/virtualenv/pull/2353
- https://github.com/pypa/virtualenv/issues/2368
Closes https://github.com/astral-sh/uv/issues/1656.
This commit is contained in:
parent
79d4a6556a
commit
41b29b2dc4
4 changed files with 312 additions and 62 deletions
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
|
@ -814,3 +814,37 @@ jobs:
|
||||||
|
|
||||||
- name: "Validate global Python install"
|
- name: "Validate global Python install"
|
||||||
run: python3 scripts/check_system_python.py --uv ./uv
|
run: python3 scripts/check_system_python.py --uv ./uv
|
||||||
|
|
||||||
|
system-test-windows-embedded-python-310:
|
||||||
|
needs: build-binary-windows
|
||||||
|
name: "check system | embedded python3.10 on windows"
|
||||||
|
runs-on: windows-latest
|
||||||
|
env:
|
||||||
|
# Avoid debug build stack overflows.
|
||||||
|
UV_STACK_SIZE: 2000000 # 2 megabyte, double the default on windows
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: "Download binary"
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: uv-windows-${{ github.sha }}
|
||||||
|
|
||||||
|
# Download embedded Python.
|
||||||
|
- name: "Download embedded Python"
|
||||||
|
run: curl -LsSf https://www.python.org/ftp/python/3.11.8/python-3.11.8-embed-amd64.zip -o python-3.11.8-embed-amd64.zip
|
||||||
|
|
||||||
|
- name: "Unzip embedded Python"
|
||||||
|
run: 7z x python-3.11.8-embed-amd64.zip -oembedded-python
|
||||||
|
|
||||||
|
- name: "Show embedded Python contents"
|
||||||
|
run: ls embedded-python
|
||||||
|
|
||||||
|
- name: "Set PATH"
|
||||||
|
run: echo "${{ github.workspace }}\embedded-python" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
|
- name: "Print Python path"
|
||||||
|
run: echo $(which python)
|
||||||
|
|
||||||
|
- name: "Validate embedded Python install"
|
||||||
|
run: python ./scripts/check_embedded_python.py --uv ./uv.exe
|
||||||
|
|
|
@ -139,6 +139,14 @@ pub fn create_bare_venv(
|
||||||
// Create a `.gitignore` file to ignore all files in the venv.
|
// Create a `.gitignore` file to ignore all files in the venv.
|
||||||
fs::write(location.join(".gitignore"), "*")?;
|
fs::write(location.join(".gitignore"), "*")?;
|
||||||
|
|
||||||
|
// Per PEP 405, the Python `home` is the parent directory of the interpreter.
|
||||||
|
let python_home = base_python.parent().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"The Python interpreter needs to have a parent directory",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
// Different names for the python interpreter
|
// Different names for the python interpreter
|
||||||
fs::create_dir(&scripts)?;
|
fs::create_dir(&scripts)?;
|
||||||
let executable = scripts.join(format!("python{EXE_SUFFIX}"));
|
let executable = scripts.join(format!("python{EXE_SUFFIX}"));
|
||||||
|
@ -163,55 +171,23 @@ pub fn create_bare_venv(
|
||||||
}
|
}
|
||||||
|
|
||||||
// No symlinking on Windows, at least not on a regular non-dev non-admin Windows install.
|
// No symlinking on Windows, at least not on a regular non-dev non-admin Windows install.
|
||||||
#[cfg(windows)]
|
if cfg!(windows) {
|
||||||
{
|
copy_launcher_windows(
|
||||||
// https://github.com/python/cpython/blob/d457345bbc6414db0443819290b04a9a4333313d/Lib/venv/__init__.py#L261-L267
|
WindowsExecutable::Python,
|
||||||
// https://github.com/pypa/virtualenv/blob/d9fdf48d69f0d0ca56140cf0381edbb5d6fe09f5/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py#L78-L83
|
interpreter,
|
||||||
// There's two kinds of applications on windows: Those that allocate a console (python.exe) and those that
|
&base_python,
|
||||||
// don't because they use window(s) (pythonw.exe).
|
&scripts,
|
||||||
for python_exe in ["python.exe", "pythonw.exe"] {
|
python_home,
|
||||||
let shim = interpreter
|
)?;
|
||||||
.stdlib()
|
copy_launcher_windows(
|
||||||
.join("venv")
|
WindowsExecutable::Pythonw,
|
||||||
.join("scripts")
|
interpreter,
|
||||||
.join("nt")
|
&base_python,
|
||||||
.join(python_exe);
|
&scripts,
|
||||||
match fs_err::copy(shim, scripts.join(python_exe)) {
|
python_home,
|
||||||
Ok(_) => {}
|
)?;
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
|
||||||
let launcher = match python_exe {
|
|
||||||
"python.exe" => "venvlauncher.exe",
|
|
||||||
"pythonw.exe" => "venvwlauncher.exe",
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// If `python.exe` doesn't exist, try the `venvlauncher.exe` shim.
|
|
||||||
let shim = interpreter
|
|
||||||
.stdlib()
|
|
||||||
.join("venv")
|
|
||||||
.join("scripts")
|
|
||||||
.join("nt")
|
|
||||||
.join(launcher);
|
|
||||||
|
|
||||||
// If the `venvlauncher.exe` shim doesn't exist, then on Conda at least, we
|
|
||||||
// can look for it next to the Python executable itself.
|
|
||||||
match fs_err::copy(shim, scripts.join(python_exe)) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
|
||||||
let shim = base_python.with_file_name(launcher);
|
|
||||||
fs_err::copy(shim, scripts.join(python_exe))?;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
return Err(err.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
return Err(err.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(unix, windows)))]
|
#[cfg(not(any(unix, windows)))]
|
||||||
{
|
{
|
||||||
compile_error!("Only Windows and Unix are supported")
|
compile_error!("Only Windows and Unix are supported")
|
||||||
|
@ -242,18 +218,6 @@ pub fn create_bare_venv(
|
||||||
fs::write(scripts.join(name), activator)?;
|
fs::write(scripts.join(name), activator)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per PEP 405, the Python `home` is the parent directory of the interpreter.
|
|
||||||
let python_home = base_python
|
|
||||||
.parent()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
io::Error::new(
|
|
||||||
io::ErrorKind::NotFound,
|
|
||||||
"The Python interpreter needs to have a parent directory",
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.simplified_display()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// Validate extra_cfg
|
// Validate extra_cfg
|
||||||
let reserved_keys = [
|
let reserved_keys = [
|
||||||
"home",
|
"home",
|
||||||
|
@ -272,7 +236,10 @@ pub fn create_bare_venv(
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pyvenv_cfg_data: Vec<(String, String)> = vec![
|
let mut pyvenv_cfg_data: Vec<(String, String)> = vec![
|
||||||
("home".to_string(), python_home),
|
(
|
||||||
|
"home".to_string(),
|
||||||
|
python_home.simplified_display().to_string(),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"implementation".to_string(),
|
"implementation".to_string(),
|
||||||
interpreter.markers().platform_python_implementation.clone(),
|
interpreter.markers().platform_python_implementation.clone(),
|
||||||
|
@ -322,3 +289,153 @@ pub fn create_bare_venv(
|
||||||
executable,
|
executable,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
enum WindowsExecutable {
|
||||||
|
/// The `python.exe` executable (or `venvlauncher.exe` launcher shim).
|
||||||
|
Python,
|
||||||
|
/// The `pythonw.exe` executable (or `venvwlauncher.exe` launcher shim).
|
||||||
|
Pythonw,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowsExecutable {
|
||||||
|
/// The name of the Python executable.
|
||||||
|
fn exe(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
WindowsExecutable::Python => "python.exe",
|
||||||
|
WindowsExecutable::Pythonw => "pythonw.exe",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of the launcher shim.
|
||||||
|
fn launcher(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
WindowsExecutable::Python => "venvlauncher.exe",
|
||||||
|
WindowsExecutable::Pythonw => "venvwlauncher.exe",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://github.com/python/cpython/blob/d457345bbc6414db0443819290b04a9a4333313d/Lib/venv/__init__.py#L261-L267>
|
||||||
|
/// <https://github.com/pypa/virtualenv/blob/d9fdf48d69f0d0ca56140cf0381edbb5d6fe09f5/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py#L78-L83>
|
||||||
|
///
|
||||||
|
/// There's two kinds of applications on windows: Those that allocate a console (python.exe)
|
||||||
|
/// and those that don't because they use window(s) (pythonw.exe).
|
||||||
|
fn copy_launcher_windows(
|
||||||
|
executable: WindowsExecutable,
|
||||||
|
interpreter: &Interpreter,
|
||||||
|
base_python: &Path,
|
||||||
|
scripts: &Path,
|
||||||
|
python_home: &Path,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// First priority: the `python.exe` and `pythonw.exe` shims.
|
||||||
|
let shim = interpreter
|
||||||
|
.stdlib()
|
||||||
|
.join("venv")
|
||||||
|
.join("scripts")
|
||||||
|
.join("nt")
|
||||||
|
.join(executable.exe());
|
||||||
|
match fs_err::copy(shim, scripts.join(executable.exe())) {
|
||||||
|
Ok(_) => return Ok(()),
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second priority: the `venvlauncher.exe` and `venvwlauncher.exe` shims.
|
||||||
|
// These are equivalent to the `python.exe` and `pythonw.exe` shims, which were
|
||||||
|
// renamed in Python 3.13.
|
||||||
|
let shim = interpreter
|
||||||
|
.stdlib()
|
||||||
|
.join("venv")
|
||||||
|
.join("scripts")
|
||||||
|
.join("nt")
|
||||||
|
.join(executable.launcher());
|
||||||
|
match fs_err::copy(shim, scripts.join(executable.exe())) {
|
||||||
|
Ok(_) => return Ok(()),
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third priority: on Conda at least, we can look for the launcher shim next to
|
||||||
|
// the Python executable itself.
|
||||||
|
let shim = base_python.with_file_name(executable.launcher());
|
||||||
|
match fs_err::copy(shim, scripts.join(executable.exe())) {
|
||||||
|
Ok(_) => return Ok(()),
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fourth priority: if the launcher shim doesn't exist, assume this is
|
||||||
|
// an embedded Python. Copy the Python executable itself, along with
|
||||||
|
// the DLLs, `.pyd` files, and `.zip` files in the same directory.
|
||||||
|
match fs_err::copy(
|
||||||
|
base_python.with_file_name(executable.exe()),
|
||||||
|
scripts.join(executable.exe()),
|
||||||
|
) {
|
||||||
|
Ok(_) => {
|
||||||
|
// Copy `.dll` and `.pyd` files from the top-level, and from the
|
||||||
|
// `DLLs` subdirectory (if it exists).
|
||||||
|
for directory in [
|
||||||
|
python_home,
|
||||||
|
interpreter.base_prefix().join("DLLs").as_path(),
|
||||||
|
] {
|
||||||
|
let entries = match fs_err::read_dir(directory) {
|
||||||
|
Ok(read_dir) => read_dir,
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().is_some_and(|ext| {
|
||||||
|
ext.eq_ignore_ascii_case("dll") || ext.eq_ignore_ascii_case("pyd")
|
||||||
|
}) {
|
||||||
|
if let Some(file_name) = path.file_name() {
|
||||||
|
fs_err::copy(&path, scripts.join(file_name))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy `.zip` files from the top-level.
|
||||||
|
match fs_err::read_dir(python_home) {
|
||||||
|
Ok(entries) => {
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
|
||||||
|
{
|
||||||
|
if let Some(file_name) = path.file_name() {
|
||||||
|
fs_err::copy(&path, scripts.join(file_name))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::NotFound(base_python.user_display().to_string()))
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use platform_tags::PlatformError;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use platform_tags::PlatformError;
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
|
|
||||||
pub use crate::bare::create_bare_venv;
|
pub use crate::bare::create_bare_venv;
|
||||||
|
@ -20,6 +20,8 @@ pub enum Error {
|
||||||
Platform(#[from] PlatformError),
|
Platform(#[from] PlatformError),
|
||||||
#[error("Reserved key used for pyvenv.cfg: {0}")]
|
#[error("Reserved key used for pyvenv.cfg: {0}")]
|
||||||
ReservedConfigKey(String),
|
ReservedConfigKey(String),
|
||||||
|
#[error("Could not find a suitable Python executable for the virtual environment based on the interpreter: {0}")]
|
||||||
|
NotFound(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The value to use for the shell prompt when inside a virtual environment.
|
/// The value to use for the shell prompt when inside a virtual environment.
|
||||||
|
|
97
scripts/check_embedded_python.py
Executable file
97
scripts/check_embedded_python.py
Executable file
|
@ -0,0 +1,97 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""Install `pylint` and `numpy` into an embedded Python."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Check an embedded Python interpreter."
|
||||||
|
)
|
||||||
|
parser.add_argument("--uv", help="Path to a uv binary.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
uv: str = os.path.abspath(args.uv) if args.uv else "uv"
|
||||||
|
|
||||||
|
# Create a temporary directory.
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
# Create a virtual environment with `uv`.
|
||||||
|
logging.info("Creating virtual environment with `uv`...")
|
||||||
|
subprocess.run(
|
||||||
|
[uv, "venv", ".venv", "--seed", "--python", sys.executable],
|
||||||
|
cwd=temp_dir,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
executable = os.path.join(temp_dir, ".venv", "Scripts", "python.exe")
|
||||||
|
else:
|
||||||
|
executable = os.path.join(temp_dir, ".venv", "bin", "python")
|
||||||
|
|
||||||
|
logging.info("Querying virtual environment...")
|
||||||
|
subprocess.run(
|
||||||
|
[executable, "--version"],
|
||||||
|
cwd=temp_dir,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info("Installing into `uv` virtual environment...")
|
||||||
|
|
||||||
|
# Disable the `CONDA_PREFIX` and `VIRTUAL_ENV` environment variables, so that
|
||||||
|
# we only rely on virtual environment discovery via the `.venv` directory.
|
||||||
|
# Our "system Python" here might itself be a Conda environment!
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["CONDA_PREFIX"] = ""
|
||||||
|
env["VIRTUAL_ENV"] = ""
|
||||||
|
|
||||||
|
# Install, verify, and uninstall a few packages.
|
||||||
|
for package in ["pylint", "numpy"]:
|
||||||
|
# Install the package.
|
||||||
|
logging.info(
|
||||||
|
f"Installing the package `{package}` into the virtual environment..."
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
[uv, "pip", "install", package, "--verbose"],
|
||||||
|
cwd=temp_dir,
|
||||||
|
check=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure that the package is installed in the virtual environment.
|
||||||
|
logging.info(f"Checking that `{package}` is installed.")
|
||||||
|
code = subprocess.run(
|
||||||
|
[executable, "-c", f"import {package}"],
|
||||||
|
cwd=temp_dir,
|
||||||
|
)
|
||||||
|
if code.returncode != 0:
|
||||||
|
raise Exception(
|
||||||
|
f"The package `{package}` isn't installed in the virtual environment."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Uninstall the package.
|
||||||
|
logging.info(f"Uninstalling the package `{package}`.")
|
||||||
|
subprocess.run(
|
||||||
|
[uv, "pip", "uninstall", package, "--verbose"],
|
||||||
|
cwd=temp_dir,
|
||||||
|
check=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure that the package isn't installed in the virtual environment.
|
||||||
|
logging.info(f"Checking that `{package}` isn't installed.")
|
||||||
|
code = subprocess.run(
|
||||||
|
[executable, "-m", "pip", "show", package],
|
||||||
|
cwd=temp_dir,
|
||||||
|
)
|
||||||
|
if code.returncode == 0:
|
||||||
|
raise Exception(
|
||||||
|
f"The package `{package}` is installed in the virtual environment (but shouldn't be)."
|
||||||
|
)
|
Loading…
Add table
Add a link
Reference in a new issue