Skip to main content

uv_virtualenv/
virtualenv.rs

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