Skip to main content

uv_virtualenv/
virtualenv.rs

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