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