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