diff --git a/crates/uv-virtualenv/src/bare.rs b/crates/uv-virtualenv/src/bare.rs index a5090fdeca31..a05ae4493ce3 100644 --- a/crates/uv-virtualenv/src/bare.rs +++ b/crates/uv-virtualenv/src/bare.rs @@ -139,6 +139,14 @@ pub fn create_bare_venv( // Create a `.gitignore` file to ignore all files in the venv. 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 fs::create_dir(&scripts)?; let executable = scripts.join(format!("python{EXE_SUFFIX}")); @@ -167,8 +175,10 @@ pub fn create_bare_venv( { // 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). + // 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). + + // First priority: the `python.exe` and `pythonw.exe` shims. for python_exe in ["python.exe", "pythonw.exe"] { let shim = interpreter .stdlib() @@ -179,13 +189,14 @@ pub fn create_bare_venv( match fs_err::copy(shim, scripts.join(python_exe)) { Ok(_) => {} Err(err) if err.kind() == io::ErrorKind::NotFound => { + // 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 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") @@ -193,13 +204,111 @@ pub fn create_bare_venv( .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 => { + // 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(launcher); - fs_err::copy(shim, scripts.join(python_exe))?; + match fs_err::copy(shim, scripts.join(python_exe)) { + Ok(_) => {} + Err(err) if err.kind() == io::ErrorKind::NotFound => { + // 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(python_exe), + scripts.join(python_exe), + ) { + Ok(_) => {} + Err(err) if err.kind() == io::ErrorKind::NotFound => { + return Err(Error::IO(io::Error::new( + io::ErrorKind::NotFound, + format!( + "Could not find a suitable Python executable for the virtual environment (tried: {}, {}, {}, {})", + interpreter + .stdlib() + .join("venv") + .join("scripts") + .join("nt") + .join(python_exe) + .user_display(), + interpreter + .stdlib() + .join("venv") + .join("scripts") + .join("nt") + .join(launcher) + .user_display(), + base_python.with_file_name(launcher).user_display(), + base_python + .with_file_name(python_exe) + .user_display(), + ) + ))); + } + Err(err) => { + return Err(err.into()); + } + } + + // 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. + let entries = match fs_err::read_dir(python_home) { + 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("zip")) + { + if let Some(file_name) = path.file_name() { + fs_err::copy(&path, scripts.join(file_name))?; + } + } + } + } + Err(err) => { + return Err(err.into()); + } + } } Err(err) => { return Err(err.into()); @@ -242,18 +351,6 @@ pub fn create_bare_venv( 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 let reserved_keys = [ "home", @@ -272,7 +369,10 @@ pub fn create_bare_venv( } 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(), interpreter.markers().platform_python_implementation.clone(), diff --git a/scripts/check_embedded_python.py b/scripts/check_embedded_python.py index 81b3f6c6c42d..ec9d9358f7de 100755 --- a/scripts/check_embedded_python.py +++ b/scripts/check_embedded_python.py @@ -13,7 +13,9 @@ if __name__ == "__main__": logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") - parser = argparse.ArgumentParser(description="Check a Python interpreter.") + parser = argparse.ArgumentParser( + description="Check an embedded Python interpreter." + ) parser.add_argument("--uv", help="Path to a uv binary.") args = parser.parse_args() @@ -49,40 +51,47 @@ env = os.environ.copy() env["CONDA_PREFIX"] = "" env["VIRTUAL_ENV"] = "" - subprocess.run( - [uv, "pip", "install", "pylint", "--verbose"], - cwd=temp_dir, - check=True, - env=env, - ) - # Ensure that the package (`pylint`) is installed in the virtual environment. - logging.info("Checking that `pylint` is installed.") - code = subprocess.run( - [executable, "-c", "import pylint"], - cwd=temp_dir, - ) - if code.returncode != 0: - raise Exception( - "The package `pylint` isn't installed in the virtual environment." + # 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, ) - # Uninstall the package (`pylint`). - logging.info("Uninstalling the package `pylint`.") - subprocess.run( - [uv, "pip", "uninstall", "pylint", "--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." + ) - # Ensure that the package (`pylint`) isn't installed in the virtual environment. - logging.info("Checking that `pylint` isn't installed.") - code = subprocess.run( - [executable, "-m", "pip", "show", "pylint"], - cwd=temp_dir, - ) - if code.returncode == 0: - raise Exception( - "The package `pylint` is installed in the virtual environment (but shouldn't be)." + # 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)." + )