//! Create a bare virtualenv without any packages install use std::env; use std::env::consts::EXE_SUFFIX; use std::io; use std::io::{BufWriter, Write}; use std::path::Path; use fs_err as fs; use fs_err::File; use pypi_types::Scheme; use tracing::info; use uv_fs::Simplified; use uv_interpreter::{Interpreter, Virtualenv}; use crate::{Error, Prompt}; /// The bash activate scripts with the venv dependent paths patches out const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[ ("activate", include_str!("activator/activate")), ("activate.csh", include_str!("activator/activate.csh")), ("activate.fish", include_str!("activator/activate.fish")), ("activate.nu", include_str!("activator/activate.nu")), ("activate.ps1", include_str!("activator/activate.ps1")), ("activate.bat", include_str!("activator/activate.bat")), ("deactivate.bat", include_str!("activator/deactivate.bat")), ("pydoc.bat", include_str!("activator/pydoc.bat")), ( "activate_this.py", include_str!("activator/activate_this.py"), ), ]; const VIRTUALENV_PATCH: &str = include_str!("_virtualenv.py"); /// Very basic `.cfg` file format writer. fn write_cfg(f: &mut impl Write, data: &[(String, String)]) -> io::Result<()> { for (key, value) in data { writeln!(f, "{key} = {value}")?; } Ok(()) } /// Write all the files that belong to a venv without any packages installed. pub fn create_bare_venv( location: &Path, interpreter: &Interpreter, prompt: Prompt, 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. uv_fs::canonicalize_executable(interpreter.sys_executable())? } else if cfg!(windows) { // On Windows, follow `virtualenv`. If we're in a virtual environment, use // `sys._base_executable` if it exists; if not, use `sys.base_prefix`. For example, with // Python installed from the Windows Store, `sys.base_prefix` is slightly "incorrect". // // 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 real and // 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 { uv_fs::canonicalize_executable(interpreter.sys_executable())? } } else { unimplemented!("Only Windows and Unix are supported") }; // Validate the existing location. match location.metadata() { Ok(metadata) => { if metadata.is_file() { return Err(Error::IO(io::Error::new( io::ErrorKind::AlreadyExists, format!("File exists at `{}`", location.user_display()), ))); } else if metadata.is_dir() { if location.join("pyvenv.cfg").is_file() { info!("Removing existing directory"); fs::remove_dir_all(location)?; fs::create_dir_all(location)?; } else if location .read_dir() .is_ok_and(|mut dir| dir.next().is_none()) { info!("Ignoring empty directory"); } else { return Err(Error::IO(io::Error::new( io::ErrorKind::AlreadyExists, format!( "The directory `{}` exists, but it's not a virtualenv", location.user_display() ), ))); } } } Err(err) if err.kind() == io::ErrorKind::NotFound => { fs::create_dir_all(location)?; } Err(err) => return Err(Error::IO(err)), } let location = location.canonicalize()?; let bin_name = if cfg!(unix) { "bin" } else if cfg!(windows) { "Scripts" } else { unimplemented!("Only Windows and Unix are supported") }; let scripts = location.join(&interpreter.virtualenv().scripts); let prompt = match prompt { Prompt::CurrentDirectoryName => env::current_dir()? .file_name() .map(|name| name.to_string_lossy().to_string()), Prompt::Static(value) => Some(value), Prompt::None => None, }; // Add the CACHEDIR.TAG. cachedir::ensure_tag(&location)?; // Create a `.gitignore` file to ignore all files in the venv. fs::write(location.join(".gitignore"), "*")?; // Different names for the python interpreter fs::create_dir(&scripts)?; let executable = scripts.join(format!("python{EXE_SUFFIX}")); #[cfg(unix)] { use fs_err::os::unix::fs::symlink; symlink(&base_python, &executable)?; symlink( "python", scripts.join(format!("python{}", interpreter.python_major())), )?; symlink( "python", scripts.join(format!( "python{}.{}", interpreter.python_major(), interpreter.python_minor(), )), )?; } // No symlinking on Windows, at least not on a regular non-dev non-admin Windows install. #[cfg(windows)] { // 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). for python_exe in ["python.exe", "pythonw.exe"] { let shim = interpreter .stdlib() .join("venv") .join("scripts") .join("nt") .join(python_exe); match fs_err::copy(shim, scripts.join(python_exe)) { Ok(_) => {} Err(err) if err.kind() == io::ErrorKind::NotFound => { let launcher = match python_exe { "python.exe" => "venvwlauncher.exe", "pythonw.exe" => "venvwlauncher.exe", _ => unreachable!(), }; // If `python.exe` doesn't exist, try the `venvlaucher.exe` shim. let shim = interpreter .stdlib() .join("venv") .join("scripts") .join("nt") .join(launcher); // If the `venvwlauncher.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)))] { compile_error!("Only Windows and Unix are supported") } // Add all the activate scripts for different shells for (name, template) in ACTIVATE_TEMPLATES { let relative_site_packages = pathdiff::diff_paths( &interpreter.virtualenv().purelib, &interpreter.virtualenv().scripts, ) .expect("Failed to calculate relative path to site-packages"); let activator = template .replace( "{{ VIRTUAL_ENV_DIR }}", // SAFETY: `unwrap` is guaranteed to succeed because `location` is an `Utf8PathBuf`. location.simplified().to_str().unwrap(), ) .replace("{{ BIN_NAME }}", bin_name) .replace( "{{ VIRTUAL_PROMPT }}", prompt.as_deref().unwrap_or_default(), ) .replace( "{{ RELATIVE_SITE_PACKAGES }}", relative_site_packages.simplified().to_str().unwrap(), ); 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", "implementation", "version_info", "include-system-site-packages", "base-prefix", "base-exec-prefix", "base-executable", "prompt", ]; for (key, _) in &extra_cfg { if reserved_keys.contains(&key.as_str()) { return Err(Error::ReservedConfigKey(key.to_string())); } } let mut pyvenv_cfg_data: Vec<(String, String)> = vec![ ("home".to_string(), python_home), ( "implementation".to_string(), interpreter.markers().platform_python_implementation.clone(), ), ( "version_info".to_string(), interpreter.markers().python_full_version.string.clone(), ), ( "include-system-site-packages".to_string(), if system_site_packages { "true".to_string() } else { "false".to_string() }, ), ] .into_iter() .chain(extra_cfg) .collect(); if let Some(prompt) = prompt { pyvenv_cfg_data.push(("prompt".to_string(), prompt)); } let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?); write_cfg(&mut pyvenv_cfg, &pyvenv_cfg_data)?; drop(pyvenv_cfg); // Construct the path to the `site-packages` directory. let site_packages = location.join(&interpreter.virtualenv().purelib); // Populate `site-packages` with a `_virtualenv.py` file. fs::create_dir_all(&site_packages)?; fs::write(site_packages.join("_virtualenv.py"), VIRTUALENV_PATCH)?; fs::write(site_packages.join("_virtualenv.pth"), "import _virtualenv")?; Ok(Virtualenv { scheme: Scheme { purelib: location.join(&interpreter.virtualenv().purelib), platlib: location.join(&interpreter.virtualenv().platlib), scripts: location.join(&interpreter.virtualenv().scripts), data: location.join(&interpreter.virtualenv().data), include: location.join(&interpreter.virtualenv().include), }, root: location, executable, }) }