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