Skip to main content

uv_virtualenv/
virtualenv.rs

1//! Create a virtual environment.
2
3use std::env::consts::EXE_SUFFIX;
4use std::io;
5use std::io::{BufWriter, Write};
6use std::path::Path;
7
8use console::Term;
9use fs_err::File;
10use itertools::Itertools;
11use owo_colors::OwoColorize;
12use tracing::{debug, trace};
13
14use crate::{Error, Prompt};
15use uv_fs::{CWD, Simplified, cachedir};
16use uv_platform_tags::Os;
17use uv_pypi_types::Scheme;
18use uv_python::managed::{
19    ManagedPythonInstallation, PythonMinorVersionLink, create_link_to_executable,
20};
21use uv_python::{Interpreter, VirtualEnvironment};
22use uv_shell::escape_posix_for_single_quotes;
23use uv_version::version;
24
25/// Activation scripts for the environment, with dependent paths templated out.
26const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[
27    ("activate", include_str!("activator/activate")),
28    ("activate.csh", include_str!("activator/activate.csh")),
29    ("activate.fish", include_str!("activator/activate.fish")),
30    ("activate.nu", include_str!("activator/activate.nu")),
31    ("activate.ps1", include_str!("activator/activate.ps1")),
32    ("activate.bat", include_str!("activator/activate.bat")),
33    ("deactivate.bat", include_str!("activator/deactivate.bat")),
34    ("pydoc.bat", include_str!("activator/pydoc.bat")),
35    (
36        "activate_this.py",
37        include_str!("activator/activate_this.py"),
38    ),
39];
40const VIRTUALENV_PATCH: &str = include_str!("_virtualenv.py");
41
42/// Very basic `.cfg` file format writer.
43fn write_cfg(f: &mut impl Write, data: &[(String, String)]) -> io::Result<()> {
44    for (key, value) in data {
45        writeln!(f, "{key} = {value}")?;
46    }
47    Ok(())
48}
49
50/// Create a [`VirtualEnvironment`] at the given location.
51#[expect(clippy::fn_params_excessive_bools)]
52pub(crate) fn create(
53    location: &Path,
54    interpreter: &Interpreter,
55    prompt: Prompt,
56    system_site_packages: bool,
57    on_existing: OnExisting,
58    relocatable: bool,
59    seed: bool,
60    upgradeable: bool,
61) -> Result<VirtualEnvironment, Error> {
62    // Determine the base Python executable; that is, the Python executable that should be
63    // considered the "base" for the virtual environment.
64    //
65    // For consistency with the standard library, rely on `sys._base_executable`, _unless_ we're
66    // using a uv-managed Python (in which case, we can do better for symlinked executables).
67    let base_python = if cfg!(unix) && interpreter.is_standalone() {
68        interpreter.find_base_python()?
69    } else {
70        interpreter.to_base_python()?
71    };
72
73    debug!(
74        "Using base executable for virtual environment: {}",
75        base_python.display()
76    );
77
78    // Extract the prompt and compute the absolute path prior to validating the location; otherwise,
79    // we risk deleting (and recreating) the current working directory, which would cause the `CWD`
80    // queries to fail.
81    let prompt = match prompt {
82        Prompt::CurrentDirectoryName => CWD
83            .file_name()
84            .map(|name| name.to_string_lossy().to_string()),
85        Prompt::Static(value) => Some(value),
86        Prompt::None => None,
87    };
88    let absolute = std::path::absolute(location)?;
89
90    // Validate the existing location.
91    match location.metadata() {
92        Ok(metadata) if metadata.is_file() => {
93            return Err(Error::Io(io::Error::new(
94                io::ErrorKind::AlreadyExists,
95                format!("File exists at `{}`", location.user_display()),
96            )));
97        }
98        Ok(metadata)
99            if metadata.is_dir()
100                && location
101                    .read_dir()
102                    .is_ok_and(|mut dir| dir.next().is_none()) =>
103        {
104            // If it's an empty directory, we can proceed
105            trace!(
106                "Using empty directory at `{}` for virtual environment",
107                location.user_display()
108            );
109        }
110        Ok(metadata) if metadata.is_dir() => {
111            let is_virtualenv = uv_fs::is_virtualenv_base(location);
112            let name = if is_virtualenv {
113                "virtual environment"
114            } else {
115                "directory"
116            };
117            let hint = format!(
118                "Use the `{}` flag or set `{}` to replace the existing {name}",
119                "--clear".green(),
120                "UV_VENV_CLEAR=1".green()
121            );
122            // TODO(zanieb): We may want to consider omitting the hint in some of these cases, e.g.,
123            // when `--no-clear` is used do we want to suggest `--clear`?
124            let err = Err(Error::Io(io::Error::new(
125                io::ErrorKind::AlreadyExists,
126                format!(
127                    "A {name} already exists at: {}\n\n{}{} {hint}",
128                    location.user_display(),
129                    "hint".bold().cyan(),
130                    ":".bold(),
131                ),
132            )));
133            match on_existing {
134                OnExisting::Allow => {
135                    debug!("Allowing existing {name} due to `--allow-existing`");
136                }
137                OnExisting::Remove(reason) => {
138                    debug!("Removing existing {name} ({reason})");
139                    // Before removing the virtual environment, we need to canonicalize the path
140                    // because `Path::metadata` will follow the symlink but we're still operating on
141                    // the unresolved path and will remove the symlink itself.
142                    let location = location
143                        .canonicalize()
144                        .unwrap_or_else(|_| location.to_path_buf());
145                    remove_virtualenv(&location)?;
146                    fs_err::create_dir_all(&location)?;
147                }
148                OnExisting::Fail => return err,
149                // If not a virtual environment, fail without prompting.
150                OnExisting::Prompt if !is_virtualenv => return err,
151                OnExisting::Prompt => {
152                    match confirm_clear(location, name)? {
153                        Some(true) => {
154                            debug!("Removing existing {name} due to confirmation");
155                            // Before removing the virtual environment, we need to canonicalize the
156                            // path because `Path::metadata` will follow the symlink but we're still
157                            // operating on the unresolved path and will remove the symlink itself.
158                            let location = location
159                                .canonicalize()
160                                .unwrap_or_else(|_| location.to_path_buf());
161                            remove_virtualenv(&location)?;
162                            fs_err::create_dir_all(&location)?;
163                        }
164                        Some(false) => return err,
165                        // When we don't have a TTY, require `--clear` explicitly.
166                        None => {
167                            return Err(Error::Exists {
168                                name,
169                                path: location.to_path_buf(),
170                            });
171                        }
172                    }
173                }
174            }
175        }
176        Ok(_) => {
177            // It's not a file or a directory
178            return Err(Error::Io(io::Error::new(
179                io::ErrorKind::AlreadyExists,
180                format!("Object already exists at `{}`", location.user_display()),
181            )));
182        }
183        Err(err) if err.kind() == io::ErrorKind::NotFound => {
184            fs_err::create_dir_all(location)?;
185        }
186        Err(err) => return Err(Error::Io(err)),
187    }
188
189    // Use the absolute path for all further operations.
190    let location = absolute;
191
192    let bin_name = if cfg!(unix) {
193        "bin"
194    } else if cfg!(windows) {
195        "Scripts"
196    } else {
197        unimplemented!("Only Windows and Unix are supported")
198    };
199    let scripts = location.join(&interpreter.virtualenv().scripts);
200
201    // Add the CACHEDIR.TAG.
202    cachedir::ensure_tag(&location)?;
203
204    // Create a `.gitignore` file to ignore all files in the venv.
205    fs_err::write(location.join(".gitignore"), "*")?;
206
207    let mut using_minor_version_link = false;
208    let executable_target = if upgradeable {
209        if let Some(minor_version_link) =
210            ManagedPythonInstallation::try_from_interpreter(interpreter)
211                .and_then(|installation| PythonMinorVersionLink::from_installation(&installation))
212        {
213            if !minor_version_link.exists() {
214                base_python.clone()
215            } else {
216                let debug_symlink_term = if cfg!(windows) {
217                    "junction"
218                } else {
219                    "symlink directory"
220                };
221                debug!(
222                    "Using {} {} instead of base Python path: {}",
223                    debug_symlink_term,
224                    &minor_version_link.symlink_directory.display(),
225                    &base_python.display()
226                );
227                using_minor_version_link = true;
228                minor_version_link.symlink_executable.clone()
229            }
230        } else {
231            base_python.clone()
232        }
233    } else {
234        base_python.clone()
235    };
236
237    // Per PEP 405, the Python `home` is the parent directory of the interpreter.
238    // For standalone interpreters, this `home` value will include a
239    // symlink directory on Unix or junction on Windows to enable transparent Python patch
240    // upgrades.
241    let python_home = executable_target
242        .parent()
243        .ok_or_else(|| {
244            io::Error::new(
245                io::ErrorKind::NotFound,
246                "The Python interpreter needs to have a parent directory",
247            )
248        })?
249        .to_path_buf();
250    let python_home = python_home.as_path();
251
252    // Different names for the python interpreter
253    fs_err::create_dir_all(&scripts)?;
254    let executable = scripts.join(format!("python{EXE_SUFFIX}"));
255
256    #[cfg(unix)]
257    {
258        uv_fs::replace_symlink(&executable_target, &executable)?;
259        uv_fs::replace_symlink(
260            "python",
261            scripts.join(format!("python{}", interpreter.python_major())),
262        )?;
263        uv_fs::replace_symlink(
264            "python",
265            scripts.join(format!(
266                "python{}.{}",
267                interpreter.python_major(),
268                interpreter.python_minor(),
269            )),
270        )?;
271        if interpreter.gil_disabled() {
272            uv_fs::replace_symlink(
273                "python",
274                scripts.join(format!(
275                    "python{}.{}t",
276                    interpreter.python_major(),
277                    interpreter.python_minor(),
278                )),
279            )?;
280        }
281
282        if interpreter.markers().implementation_name() == "pypy" {
283            uv_fs::replace_symlink(
284                "python",
285                scripts.join(format!("pypy{}", interpreter.python_major())),
286            )?;
287            uv_fs::replace_symlink("python", scripts.join("pypy"))?;
288        }
289
290        if interpreter.markers().implementation_name() == "graalpy" {
291            uv_fs::replace_symlink("python", scripts.join("graalpy"))?;
292        }
293    }
294
295    // On Windows, we use trampolines that point to an executable target. For standalone
296    // interpreters, this target path includes a minor version junction to enable
297    // transparent upgrades.
298    if cfg!(windows) {
299        if using_minor_version_link {
300            let target = scripts.join(WindowsExecutable::Python.exe(interpreter));
301            create_link_to_executable(target.as_path(), &executable_target)
302                .map_err(Error::Python)?;
303            let targetw = scripts.join(WindowsExecutable::Pythonw.exe(interpreter));
304            create_link_to_executable(targetw.as_path(), &executable_target)
305                .map_err(Error::Python)?;
306            if interpreter.gil_disabled() {
307                let targett = scripts.join(WindowsExecutable::PythonMajorMinort.exe(interpreter));
308                create_link_to_executable(targett.as_path(), &executable_target)
309                    .map_err(Error::Python)?;
310                let targetwt = scripts.join(WindowsExecutable::PythonwMajorMinort.exe(interpreter));
311                create_link_to_executable(targetwt.as_path(), &executable_target)
312                    .map_err(Error::Python)?;
313            }
314        } else if matches!(interpreter.platform().os(), Os::Pyodide { .. }) {
315            // For Pyodide, link only `python.exe`.
316            // This should not be copied as `python.exe` is a wrapper that launches Pyodide.
317            let target = scripts.join(WindowsExecutable::Python.exe(interpreter));
318            create_link_to_executable(target.as_path(), &executable_target)
319                .map_err(Error::Python)?;
320        } else {
321            // Always copy `python.exe`.
322            copy_launcher_windows(
323                WindowsExecutable::Python,
324                interpreter,
325                &base_python,
326                &scripts,
327                python_home,
328            )?;
329
330            match interpreter.implementation_name() {
331                "graalpy" => {
332                    // For GraalPy, copy `graalpy.exe` and `python3.exe`.
333                    copy_launcher_windows(
334                        WindowsExecutable::GraalPy,
335                        interpreter,
336                        &base_python,
337                        &scripts,
338                        python_home,
339                    )?;
340                    copy_launcher_windows(
341                        WindowsExecutable::PythonMajor,
342                        interpreter,
343                        &base_python,
344                        &scripts,
345                        python_home,
346                    )?;
347                }
348                "pypy" => {
349                    // For PyPy, copy all versioned executables and all PyPy-specific executables.
350                    copy_launcher_windows(
351                        WindowsExecutable::PythonMajor,
352                        interpreter,
353                        &base_python,
354                        &scripts,
355                        python_home,
356                    )?;
357                    copy_launcher_windows(
358                        WindowsExecutable::PythonMajorMinor,
359                        interpreter,
360                        &base_python,
361                        &scripts,
362                        python_home,
363                    )?;
364                    copy_launcher_windows(
365                        WindowsExecutable::Pythonw,
366                        interpreter,
367                        &base_python,
368                        &scripts,
369                        python_home,
370                    )?;
371                    copy_launcher_windows(
372                        WindowsExecutable::PyPy,
373                        interpreter,
374                        &base_python,
375                        &scripts,
376                        python_home,
377                    )?;
378                    copy_launcher_windows(
379                        WindowsExecutable::PyPyMajor,
380                        interpreter,
381                        &base_python,
382                        &scripts,
383                        python_home,
384                    )?;
385                    copy_launcher_windows(
386                        WindowsExecutable::PyPyMajorMinor,
387                        interpreter,
388                        &base_python,
389                        &scripts,
390                        python_home,
391                    )?;
392                    copy_launcher_windows(
393                        WindowsExecutable::PyPyw,
394                        interpreter,
395                        &base_python,
396                        &scripts,
397                        python_home,
398                    )?;
399                    copy_launcher_windows(
400                        WindowsExecutable::PyPyMajorMinorw,
401                        interpreter,
402                        &base_python,
403                        &scripts,
404                        python_home,
405                    )?;
406                }
407                _ => {
408                    // For all other interpreters, copy `pythonw.exe`.
409                    copy_launcher_windows(
410                        WindowsExecutable::Pythonw,
411                        interpreter,
412                        &base_python,
413                        &scripts,
414                        python_home,
415                    )?;
416
417                    // If the GIL is disabled, copy `venvlaunchert.exe` and `venvwlaunchert.exe`.
418                    if interpreter.gil_disabled() {
419                        copy_launcher_windows(
420                            WindowsExecutable::PythonMajorMinort,
421                            interpreter,
422                            &base_python,
423                            &scripts,
424                            python_home,
425                        )?;
426                        copy_launcher_windows(
427                            WindowsExecutable::PythonwMajorMinort,
428                            interpreter,
429                            &base_python,
430                            &scripts,
431                            python_home,
432                        )?;
433                    }
434                }
435            }
436        }
437    }
438
439    #[cfg(not(any(unix, windows)))]
440    {
441        compile_error!("Only Windows and Unix are supported")
442    }
443
444    // Add all the activate scripts for different shells
445    for (name, template) in ACTIVATE_TEMPLATES {
446        // csh has no way to determine its own script location, so a relocatable
447        // activate.csh is not possible. Skip it entirely instead of generating a
448        // non-functional script.
449        if relocatable && *name == "activate.csh" {
450            continue;
451        }
452
453        let path_sep = if cfg!(windows) { ";" } else { ":" };
454
455        let relative_site_packages = [
456            interpreter.virtualenv().purelib.as_path(),
457            interpreter.virtualenv().platlib.as_path(),
458        ]
459        .iter()
460        .dedup()
461        .map(|path| {
462            pathdiff::diff_paths(path, &interpreter.virtualenv().scripts)
463                .expect("Failed to calculate relative path to site-packages")
464        })
465        .map(|path| path.simplified().to_str().unwrap().replace('\\', "\\\\"))
466        .join(path_sep);
467
468        let virtual_env_dir = match (relocatable, name.to_owned()) {
469            (true, "activate") => {
470                r#"'"$(dirname -- "$(dirname -- "$(realpath -- "$SCRIPT_PATH")")")"'"#.to_string()
471            }
472            (true, "activate.bat") => r"%~dp0..".to_string(),
473            (true, "activate.fish") => {
474                r#"'"$(dirname -- "$(cd "$(dirname -- "$(status -f)")"; and pwd)")"'"#.to_string()
475            }
476            (true, "activate.nu") => r"(path self | path dirname | path dirname)".to_string(),
477            (false, "activate.nu") => {
478                format!(
479                    "'{}'",
480                    escape_posix_for_single_quotes(location.simplified().to_str().unwrap())
481                )
482            }
483            // Note: `activate.ps1` is already relocatable by default.
484            _ => escape_posix_for_single_quotes(location.simplified().to_str().unwrap()),
485        };
486
487        let activator = template
488            .replace("{{ VIRTUAL_ENV_DIR }}", &virtual_env_dir)
489            .replace("{{ BIN_NAME }}", bin_name)
490            .replace(
491                "{{ VIRTUAL_PROMPT }}",
492                prompt.as_deref().unwrap_or_default(),
493            )
494            .replace("{{ PATH_SEP }}", path_sep)
495            .replace("{{ RELATIVE_SITE_PACKAGES }}", &relative_site_packages);
496        fs_err::write(scripts.join(name), activator)?;
497    }
498
499    let mut pyvenv_cfg_data: Vec<(String, String)> = vec![
500        (
501            "home".to_string(),
502            python_home.simplified_display().to_string(),
503        ),
504        (
505            "implementation".to_string(),
506            interpreter
507                .markers()
508                .platform_python_implementation()
509                .to_string(),
510        ),
511        ("uv".to_string(), version().to_string()),
512        (
513            "version_info".to_string(),
514            interpreter.markers().python_full_version().string.clone(),
515        ),
516        (
517            "include-system-site-packages".to_string(),
518            if system_site_packages {
519                "true".to_string()
520            } else {
521                "false".to_string()
522            },
523        ),
524    ];
525
526    if relocatable {
527        pyvenv_cfg_data.push(("relocatable".to_string(), "true".to_string()));
528    }
529
530    if seed {
531        pyvenv_cfg_data.push(("seed".to_string(), "true".to_string()));
532    }
533
534    if let Some(prompt) = prompt {
535        pyvenv_cfg_data.push(("prompt".to_string(), prompt));
536    }
537
538    if cfg!(windows) && interpreter.markers().implementation_name() == "graalpy" {
539        pyvenv_cfg_data.push((
540            "venvlauncher_command".to_string(),
541            python_home
542                .join("graalpy.exe")
543                .simplified_display()
544                .to_string(),
545        ));
546    }
547
548    let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?);
549    write_cfg(&mut pyvenv_cfg, &pyvenv_cfg_data)?;
550    drop(pyvenv_cfg);
551
552    // Construct the path to the `site-packages` directory.
553    let site_packages = location.join(&interpreter.virtualenv().purelib);
554    fs_err::create_dir_all(&site_packages)?;
555
556    // If necessary, create a symlink from `lib64` to `lib`.
557    // See: https://github.com/python/cpython/blob/b228655c227b2ca298a8ffac44d14ce3d22f6faa/Lib/venv/__init__.py#L135C11-L135C16
558    #[cfg(unix)]
559    if interpreter.pointer_size().is_64()
560        && interpreter.markers().os_name() == "posix"
561        && interpreter.markers().sys_platform() != "darwin"
562    {
563        match fs_err::os::unix::fs::symlink("lib", location.join("lib64")) {
564            Ok(()) => {}
565            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
566            Err(err) => {
567                return Err(err.into());
568            }
569        }
570    }
571
572    // Populate `site-packages` with a `_virtualenv.py` file.
573    fs_err::write(site_packages.join("_virtualenv.py"), VIRTUALENV_PATCH)?;
574    fs_err::write(site_packages.join("_virtualenv.pth"), "import _virtualenv")?;
575
576    Ok(VirtualEnvironment {
577        scheme: Scheme {
578            purelib: location.join(&interpreter.virtualenv().purelib),
579            platlib: location.join(&interpreter.virtualenv().platlib),
580            scripts: location.join(&interpreter.virtualenv().scripts),
581            data: location.join(&interpreter.virtualenv().data),
582            include: location.join(&interpreter.virtualenv().include),
583        },
584        root: location,
585        executable,
586        base_executable: base_python,
587    })
588}
589
590/// Prompt a confirmation that the virtual environment should be cleared.
591///
592/// If not a TTY, returns `None`.
593fn confirm_clear(location: &Path, name: &'static str) -> Result<Option<bool>, io::Error> {
594    let term = Term::stderr();
595    if term.is_term() {
596        let prompt = format!(
597            "A {name} already exists at `{}`. Do you want to replace it?",
598            location.user_display(),
599        );
600        let hint = format!(
601            "Use the `{}` flag or set `{}` to skip this prompt",
602            "--clear".green(),
603            "UV_VENV_CLEAR=1".green()
604        );
605        Ok(Some(uv_console::confirm_with_hint(
606            &prompt, &hint, &term, true,
607        )?))
608    } else {
609        Ok(None)
610    }
611}
612
613/// Perform a safe removal of a virtual environment.
614pub fn remove_virtualenv(location: &Path) -> Result<(), Error> {
615    // On Windows, if the current executable is in the directory, defer self-deletion since Windows
616    // won't let you unlink a running executable.
617    #[cfg(windows)]
618    if let Ok(itself) = std::env::current_exe() {
619        let target = std::path::absolute(location)?;
620        if itself.starts_with(&target) {
621            debug!("Detected self-delete of executable: {}", itself.display());
622            self_replace::self_delete_outside_path(location)?;
623        }
624    }
625
626    // We defer removal of the `pyvenv.cfg` until the end, so if we fail to remove the environment,
627    // uv can still identify it as a Python virtual environment that can be deleted.
628    for entry in fs_err::read_dir(location)? {
629        let entry = entry?;
630        let path = entry.path();
631        if path == location.join("pyvenv.cfg") {
632            continue;
633        }
634        if path.is_dir() {
635            fs_err::remove_dir_all(&path)?;
636        } else {
637            fs_err::remove_file(&path)?;
638        }
639    }
640
641    match fs_err::remove_file(location.join("pyvenv.cfg")) {
642        Ok(()) => {}
643        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
644        Err(err) => return Err(err.into()),
645    }
646
647    // Remove the virtual environment directory itself
648    match fs_err::remove_dir_all(location) {
649        Ok(()) => {}
650        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
651        // If the virtual environment is a mounted file system, e.g., in a Docker container, we
652        // cannot delete it — but that doesn't need to be a fatal error
653        Err(err) if err.kind() == io::ErrorKind::ResourceBusy => {
654            debug!(
655                "Skipping removal of `{}` directory due to {err}",
656                location.display(),
657            );
658        }
659        Err(err) => return Err(err.into()),
660    }
661
662    Ok(())
663}
664
665#[derive(Debug, Copy, Clone, Eq, PartialEq)]
666pub enum RemovalReason {
667    /// The removal was explicitly requested, i.e., with `--clear`.
668    UserRequest,
669    /// The environment can be removed because it is considered temporary, e.g., a build
670    /// environment.
671    TemporaryEnvironment,
672    /// The environment can be removed because it is managed by uv, e.g., a project or tool
673    /// environment.
674    ManagedEnvironment,
675}
676
677impl std::fmt::Display for RemovalReason {
678    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
679        match self {
680            Self::UserRequest => f.write_str("requested with `--clear`"),
681            Self::ManagedEnvironment => f.write_str("environment is managed by uv"),
682            Self::TemporaryEnvironment => f.write_str("environment is temporary"),
683        }
684    }
685}
686
687#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
688pub enum OnExisting {
689    /// Prompt before removing an existing directory.
690    ///
691    /// If a TTY is not available, fail.
692    #[default]
693    Prompt,
694    /// Fail if the directory already exists and is non-empty.
695    Fail,
696    /// Allow an existing directory, overwriting virtual environment files while retaining other
697    /// files in the directory.
698    Allow,
699    /// Remove an existing directory.
700    Remove(RemovalReason),
701}
702
703impl OnExisting {
704    pub fn from_args(allow_existing: bool, clear: bool, no_clear: bool) -> Self {
705        if allow_existing {
706            Self::Allow
707        } else if clear {
708            Self::Remove(RemovalReason::UserRequest)
709        } else if no_clear {
710            Self::Fail
711        } else {
712            Self::Prompt
713        }
714    }
715}
716
717#[derive(Debug, Copy, Clone)]
718enum WindowsExecutable {
719    /// The `python.exe` executable (or `venvlauncher.exe` launcher shim).
720    Python,
721    /// The `python3.exe` executable (or `venvlauncher.exe` launcher shim).
722    PythonMajor,
723    /// The `python3.<minor>.exe` executable (or `venvlauncher.exe` launcher shim).
724    PythonMajorMinor,
725    /// The `python3.<minor>t.exe` executable (or `venvlaunchert.exe` launcher shim).
726    PythonMajorMinort,
727    /// The `pythonw.exe` executable (or `venvwlauncher.exe` launcher shim).
728    Pythonw,
729    /// The `pythonw3.<minor>t.exe` executable (or `venvwlaunchert.exe` launcher shim).
730    PythonwMajorMinort,
731    /// The `pypy.exe` executable.
732    PyPy,
733    /// The `pypy3.exe` executable.
734    PyPyMajor,
735    /// The `pypy3.<minor>.exe` executable.
736    PyPyMajorMinor,
737    /// The `pypyw.exe` executable.
738    PyPyw,
739    /// The `pypy3.<minor>w.exe` executable.
740    PyPyMajorMinorw,
741    /// The `graalpy.exe` executable.
742    GraalPy,
743}
744
745impl WindowsExecutable {
746    /// The name of the Python executable.
747    fn exe(self, interpreter: &Interpreter) -> String {
748        match self {
749            Self::Python => String::from("python.exe"),
750            Self::PythonMajor => {
751                format!("python{}.exe", interpreter.python_major())
752            }
753            Self::PythonMajorMinor => {
754                format!(
755                    "python{}.{}.exe",
756                    interpreter.python_major(),
757                    interpreter.python_minor()
758                )
759            }
760            Self::PythonMajorMinort => {
761                format!(
762                    "python{}.{}t.exe",
763                    interpreter.python_major(),
764                    interpreter.python_minor()
765                )
766            }
767            Self::Pythonw => String::from("pythonw.exe"),
768            Self::PythonwMajorMinort => {
769                format!(
770                    "pythonw{}.{}t.exe",
771                    interpreter.python_major(),
772                    interpreter.python_minor()
773                )
774            }
775            Self::PyPy => String::from("pypy.exe"),
776            Self::PyPyMajor => {
777                format!("pypy{}.exe", interpreter.python_major())
778            }
779            Self::PyPyMajorMinor => {
780                format!(
781                    "pypy{}.{}.exe",
782                    interpreter.python_major(),
783                    interpreter.python_minor()
784                )
785            }
786            Self::PyPyw => String::from("pypyw.exe"),
787            Self::PyPyMajorMinorw => {
788                format!(
789                    "pypy{}.{}w.exe",
790                    interpreter.python_major(),
791                    interpreter.python_minor()
792                )
793            }
794            Self::GraalPy => String::from("graalpy.exe"),
795        }
796    }
797
798    /// The name of the launcher shim.
799    fn launcher(self, interpreter: &Interpreter) -> &'static str {
800        match self {
801            Self::Python | Self::PythonMajor | Self::PythonMajorMinor
802                if interpreter.gil_disabled() =>
803            {
804                "venvlaunchert.exe"
805            }
806            Self::Python | Self::PythonMajor | Self::PythonMajorMinor => "venvlauncher.exe",
807            Self::Pythonw if interpreter.gil_disabled() => "venvwlaunchert.exe",
808            Self::Pythonw => "venvwlauncher.exe",
809            Self::PythonMajorMinort => "venvlaunchert.exe",
810            Self::PythonwMajorMinort => "venvwlaunchert.exe",
811            // From 3.13 on these should replace the `python.exe` and `pythonw.exe` shims.
812            // These are not relevant as of now for PyPy as it doesn't yet support Python 3.13.
813            Self::PyPy | Self::PyPyMajor | Self::PyPyMajorMinor => "venvlauncher.exe",
814            Self::PyPyw | Self::PyPyMajorMinorw => "venvwlauncher.exe",
815            Self::GraalPy => "venvlauncher.exe",
816        }
817    }
818}
819
820/// <https://github.com/python/cpython/blob/d457345bbc6414db0443819290b04a9a4333313d/Lib/venv/__init__.py#L261-L267>
821/// <https://github.com/pypa/virtualenv/blob/d9fdf48d69f0d0ca56140cf0381edbb5d6fe09f5/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py#L78-L83>
822///
823/// There's two kinds of applications on windows: Those that allocate a console (python.exe)
824/// and those that don't because they use window(s) (pythonw.exe).
825fn copy_launcher_windows(
826    executable: WindowsExecutable,
827    interpreter: &Interpreter,
828    base_python: &Path,
829    scripts: &Path,
830    python_home: &Path,
831) -> Result<(), Error> {
832    // First priority: the `python.exe` and `pythonw.exe` shims.
833    let shim = interpreter
834        .stdlib()
835        .join("venv")
836        .join("scripts")
837        .join("nt")
838        .join(executable.exe(interpreter));
839    match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
840        Ok(_) => return Ok(()),
841        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
842        Err(err) => {
843            return Err(err.into());
844        }
845    }
846
847    // Second priority: the `venvlauncher.exe` and `venvwlauncher.exe` shims.
848    // These are equivalent to the `python.exe` and `pythonw.exe` shims, which were
849    // renamed in Python 3.13.
850    let shim = interpreter
851        .stdlib()
852        .join("venv")
853        .join("scripts")
854        .join("nt")
855        .join(executable.launcher(interpreter));
856    match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
857        Ok(_) => return Ok(()),
858        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
859        Err(err) => {
860            return Err(err.into());
861        }
862    }
863
864    // Third priority: on Conda at least, we can look for the launcher shim next to
865    // the Python executable itself.
866    let shim = base_python.with_file_name(executable.launcher(interpreter));
867    match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
868        Ok(_) => return Ok(()),
869        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
870        Err(err) => {
871            return Err(err.into());
872        }
873    }
874
875    // Fourth priority: if the launcher shim doesn't exist, assume this is
876    // an embedded Python. Copy the Python executable itself, along with
877    // the DLLs, `.pyd` files, and `.zip` files in the same directory.
878    match fs_err::copy(
879        base_python.with_file_name(executable.exe(interpreter)),
880        scripts.join(executable.exe(interpreter)),
881    ) {
882        Ok(_) => {
883            // Copy `.dll` and `.pyd` files from the top-level, and from the
884            // `DLLs` subdirectory (if it exists).
885            for directory in [
886                python_home,
887                interpreter.sys_base_prefix().join("DLLs").as_path(),
888            ] {
889                let entries = match fs_err::read_dir(directory) {
890                    Ok(read_dir) => read_dir,
891                    Err(err) if err.kind() == io::ErrorKind::NotFound => {
892                        continue;
893                    }
894                    Err(err) => {
895                        return Err(err.into());
896                    }
897                };
898                for entry in entries {
899                    let entry = entry?;
900                    let path = entry.path();
901                    if path.extension().is_some_and(|ext| {
902                        ext.eq_ignore_ascii_case("dll") || ext.eq_ignore_ascii_case("pyd")
903                    }) {
904                        if let Some(file_name) = path.file_name() {
905                            fs_err::copy(&path, scripts.join(file_name))?;
906                        }
907                    }
908                }
909            }
910
911            // Copy `.zip` files from the top-level.
912            match fs_err::read_dir(python_home) {
913                Ok(entries) => {
914                    for entry in entries {
915                        let entry = entry?;
916                        let path = entry.path();
917                        if path
918                            .extension()
919                            .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
920                        {
921                            if let Some(file_name) = path.file_name() {
922                                fs_err::copy(&path, scripts.join(file_name))?;
923                            }
924                        }
925                    }
926                }
927                Err(err) if err.kind() == io::ErrorKind::NotFound => {}
928                Err(err) => {
929                    return Err(err.into());
930                }
931            }
932
933            return Ok(());
934        }
935        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
936        Err(err) => {
937            return Err(err.into());
938        }
939    }
940
941    Err(Error::NotFound(base_python.user_display().to_string()))
942}