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