From e57b4ced8b570ff3eb9d5da70db302b7bda28a17 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 2 Mar 2024 16:04:57 -0500 Subject: [PATCH] Add a test --- crates/uv-virtualenv/src/bare.rs | 23 +++++++++-- crates/uv/tests/venv.rs | 69 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/crates/uv-virtualenv/src/bare.rs b/crates/uv-virtualenv/src/bare.rs index f3bcc6e9db31..7513be1ea81e 100644 --- a/crates/uv-virtualenv/src/bare.rs +++ b/crates/uv-virtualenv/src/bare.rs @@ -48,11 +48,28 @@ pub fn create_bare_venv( system_site_packages: bool, extra_cfg: Vec<(String, String)>, ) -> Result { + // Determine the base Python executable; that is, the Python executable that should be + // considered the "base" for the virtual environment. This is typically the Python executable + // from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then + // the base Python executable is the Python executable of the interpreter's base interpreter. let base_python = if cfg!(unix) { + // On Unix, follow symlinks to resolve the base interpreter, since the Python executable in + // a virtual environment is a symlink to the base interpreter. fs_err::canonicalize(interpreter.sys_executable())? } else if cfg!(windows) { - if let Some(base_executable) = interpreter.base_executable() { - base_executable.to_path_buf() + // On Windows, follow `virtualenv`. If we're in a virtual environment, use + // `sys._base_executable` if it exists; if not, use `sys.base_prefix`. If we're _not_ in a + // virtual environment, use the interpreter's executable, since it's already a "system + // Python". We canonicalize the path to ensure that it's consistent, though we don't expect + // any symlinks on Windows. + if interpreter.is_virtualenv() { + if let Some(base_executable) = interpreter.base_executable() { + base_executable.to_path_buf() + } else { + // Assume `python.exe`, though the exact executable name is never used (below) on + // Windows, only its parent directory. + interpreter.base_prefix().join("python.exe") + } } else { fs_err::canonicalize(interpreter.sys_executable())? } @@ -202,7 +219,7 @@ pub fn create_bare_venv( .ok_or_else(|| { io::Error::new( io::ErrorKind::NotFound, - "The python interpreter needs to have a parent directory", + "The Python interpreter needs to have a parent directory", ) })? .simplified_display() diff --git a/crates/uv/tests/venv.rs b/crates/uv/tests/venv.rs index ea3c213fa545..4860cd2ebcd4 100644 --- a/crates/uv/tests/venv.rs +++ b/crates/uv/tests/venv.rs @@ -1,8 +1,10 @@ #![cfg(feature = "python")] +use std::fs; use std::process::Command; use anyhow::Result; +use assert_cmd::prelude::*; use assert_fs::prelude::*; use uv_fs::Simplified; @@ -663,3 +665,70 @@ fn verify_pyvenv_cfg() { let search_string = format!("uv = {version}"); pyvenv_cfg.assert(predicates::str::contains(search_string)); } + +/// Ensure that a nested virtual environment uses the same `home` directory as the parent. +#[test] +fn verify_nested_pyvenv_cfg() -> Result<()> { + let temp_dir = assert_fs::TempDir::new()?; + let cache_dir = assert_fs::TempDir::new()?; + let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); + let venv = temp_dir.child(".venv"); + + // Create a virtual environment at `.venv`. + Command::new(get_bin()) + .arg("venv") + .arg(venv.as_os_str()) + .arg("--python") + .arg("3.12") + .arg("--cache-dir") + .arg(cache_dir.path()) + .arg("--exclude-newer") + .arg(EXCLUDE_NEWER) + .env("UV_TEST_PYTHON_PATH", bin.clone()) + .current_dir(&temp_dir) + .assert() + .success(); + + let pyvenv_cfg = venv.child("pyvenv.cfg"); + + // Check pyvenv.cfg exists + pyvenv_cfg.assert(predicates::path::is_file()); + + // Extract the "home" line from the pyvenv.cfg file. + let contents = fs::read_to_string(pyvenv_cfg.path())?; + let venv_home = contents + .lines() + .find(|line| line.starts_with("home")) + .expect("home line not found"); + + // Now, create a virtual environment from within the virtual environment. + let subvenv = temp_dir.child(".subvenv"); + Command::new(get_bin()) + .arg("venv") + .arg(subvenv.as_os_str()) + .arg("--python") + .arg("3.12") + .arg("--cache-dir") + .arg(cache_dir.path()) + .arg("--exclude-newer") + .arg(EXCLUDE_NEWER) + .env("VIRTUAL_ENV", venv.as_os_str()) + .env("UV_TEST_PYTHON_PATH", bin.clone()) + .current_dir(&temp_dir) + .assert() + .success(); + + let sub_pyvenv_cfg = subvenv.child("pyvenv.cfg"); + + // Extract the "home" line from the pyvenv.cfg file. + let contents = fs::read_to_string(sub_pyvenv_cfg.path())?; + let sub_venv_home = contents + .lines() + .find(|line| line.starts_with("home")) + .expect("home line not found"); + + // Check that both directories point to the same home. + assert_eq!(sub_venv_home, venv_home); + + Ok(()) +}