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