1use 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;
12
13use tracing::{debug, trace};
14
15use crate::{Error, Prompt};
16use uv_fs::{CWD, Simplified, cachedir};
17use uv_platform_tags::Os;
18use uv_pypi_types::Scheme;
19use uv_python::managed::{
20 ManagedPythonInstallation, PythonMinorVersionLink, replace_link_to_executable,
21};
22use uv_python::{Interpreter, VirtualEnvironment};
23use uv_shell::escape_posix_for_single_quotes;
24use uv_version::version;
25use uv_warnings::warn_user_once;
26
27const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[
29 ("activate", include_str!("activator/activate")),
30 ("activate.csh", include_str!("activator/activate.csh")),
31 ("activate.fish", include_str!("activator/activate.fish")),
32 ("activate.nu", include_str!("activator/activate.nu")),
33 ("activate.ps1", include_str!("activator/activate.ps1")),
34 ("activate.bat", include_str!("activator/activate.bat")),
35 ("deactivate.bat", include_str!("activator/deactivate.bat")),
36 ("pydoc.bat", include_str!("activator/pydoc.bat")),
37 (
38 "activate_this.py",
39 include_str!("activator/activate_this.py"),
40 ),
41];
42const VIRTUALENV_PATCH: &str = include_str!("_virtualenv.py");
43
44fn write_cfg(f: &mut impl Write, data: &[(String, String)]) -> io::Result<()> {
46 for (key, value) in data {
47 writeln!(f, "{key} = {value}")?;
48 }
49 Ok(())
50}
51
52#[expect(clippy::fn_params_excessive_bools)]
54pub(crate) fn create(
55 location: &Path,
56 interpreter: &Interpreter,
57 prompt: Prompt,
58 system_site_packages: bool,
59 on_existing: OnExisting,
60 relocatable: bool,
61 seed: bool,
62 upgradeable: bool,
63) -> Result<VirtualEnvironment, Error> {
64 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 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 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 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 err = Err(Error::Exists {
122 name,
123 path: location.to_path_buf(),
124 });
125 match on_existing {
126 OnExisting::Allow => {
127 debug!("Allowing existing {name} due to `--allow-existing`");
128 }
129 OnExisting::Remove(reason) => {
130 if !is_virtualenv
131 && let RemovalReason::UserRequest(clear_non_virtualenv) = reason
132 {
133 match clear_non_virtualenv {
134 ClearNonVirtualenv::Allow => {}
135 ClearNonVirtualenv::Warn => {
136 warn_user_once!(
137 "The `--clear` option will remove the existing directory at `{}` \
138 even though it is not a virtual environment. \
139 This will become an error in a future release. \
140 Use `--force` to suppress this warning, or \
141 `--preview-features venv-safe-clear` to error on this now.",
142 location.user_display()
143 );
144 }
145 ClearNonVirtualenv::Error => {
146 return Err(Error::ClearNonVirtualenv {
147 path: location.to_path_buf(),
148 });
149 }
150 }
151 }
152 debug!("Removing existing {name} ({reason})");
153 let location = location
157 .canonicalize()
158 .unwrap_or_else(|_| location.to_path_buf());
159 uv_fs::remove_virtualenv(&location)?;
160 fs_err::create_dir_all(&location)?;
161 }
162 OnExisting::Fail => return err,
163 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 let location = location
173 .canonicalize()
174 .unwrap_or_else(|_| location.to_path_buf());
175 uv_fs::remove_virtualenv(&location)?;
176 fs_err::create_dir_all(&location)?;
177 }
178 Some(false) => return err,
179 None => {
181 return Err(Error::Exists {
182 name,
183 path: location.to_path_buf(),
184 });
185 }
186 }
187 }
188 }
189 }
190 Ok(_) => {
191 return Err(Error::Io(io::Error::new(
193 io::ErrorKind::AlreadyExists,
194 format!("Object already exists at `{}`", location.user_display()),
195 )));
196 }
197 Err(err) if err.kind() == io::ErrorKind::NotFound => {
198 fs_err::create_dir_all(location)?;
199 }
200 Err(err) => return Err(Error::Io(err)),
201 }
202
203 let location = absolute;
205
206 let bin_name = if cfg!(unix) {
207 "bin"
208 } else if cfg!(windows) {
209 "Scripts"
210 } else {
211 unimplemented!("Only Windows and Unix are supported")
212 };
213 let scripts = location.join(&interpreter.virtualenv().scripts);
214
215 cachedir::ensure_tag(&location)?;
217
218 fs_err::write(location.join(".gitignore"), "*")?;
220
221 let mut using_minor_version_link = false;
222 let executable_target = if upgradeable {
223 if let Some(minor_version_link) =
224 ManagedPythonInstallation::try_from_interpreter(interpreter)
225 .and_then(|installation| PythonMinorVersionLink::from_installation(&installation))
226 {
227 if !minor_version_link.exists() {
228 base_python.clone()
229 } else {
230 let debug_symlink_term = if cfg!(windows) {
231 "junction"
232 } else {
233 "symlink directory"
234 };
235 debug!(
236 "Using {} {} instead of base Python path: {}",
237 debug_symlink_term,
238 &minor_version_link.symlink_directory.display(),
239 &base_python.display()
240 );
241 using_minor_version_link = true;
242 minor_version_link.symlink_executable.clone()
243 }
244 } else {
245 base_python.clone()
246 }
247 } else {
248 base_python.clone()
249 };
250
251 let python_home = executable_target
256 .parent()
257 .ok_or_else(|| {
258 io::Error::new(
259 io::ErrorKind::NotFound,
260 "The Python interpreter needs to have a parent directory",
261 )
262 })?
263 .to_path_buf();
264 let python_home = python_home.as_path();
265
266 fs_err::create_dir_all(&scripts)?;
268 let executable = scripts.join(format!("python{EXE_SUFFIX}"));
269
270 #[cfg(unix)]
271 {
272 uv_fs::replace_symlink(&executable_target, &executable)?;
273 uv_fs::replace_symlink(
274 "python",
275 scripts.join(format!("python{}", interpreter.python_major())),
276 )?;
277 uv_fs::replace_symlink(
278 "python",
279 scripts.join(format!(
280 "python{}.{}",
281 interpreter.python_major(),
282 interpreter.python_minor(),
283 )),
284 )?;
285 if interpreter.gil_disabled() {
286 uv_fs::replace_symlink(
287 "python",
288 scripts.join(format!(
289 "python{}.{}t",
290 interpreter.python_major(),
291 interpreter.python_minor(),
292 )),
293 )?;
294 }
295
296 if interpreter.markers().implementation_name() == "pypy" {
297 uv_fs::replace_symlink(
298 "python",
299 scripts.join(format!("pypy{}", interpreter.python_major())),
300 )?;
301 uv_fs::replace_symlink("python", scripts.join("pypy"))?;
302 }
303
304 if interpreter.markers().implementation_name() == "graalpy" {
305 uv_fs::replace_symlink("python", scripts.join("graalpy"))?;
306 }
307 }
308
309 if cfg!(windows) {
313 if using_minor_version_link {
314 let target = scripts.join(WindowsExecutable::Python.exe(interpreter));
315 replace_link_to_executable(target.as_path(), &executable_target)
316 .map_err(Error::Python)?;
317 let targetw = scripts.join(WindowsExecutable::Pythonw.exe(interpreter));
318 replace_link_to_executable(targetw.as_path(), &executable_target)
319 .map_err(Error::Python)?;
320 if interpreter.gil_disabled() {
321 let targett = scripts.join(WindowsExecutable::PythonMajorMinort.exe(interpreter));
322 replace_link_to_executable(targett.as_path(), &executable_target)
323 .map_err(Error::Python)?;
324 let targetwt = scripts.join(WindowsExecutable::PythonwMajorMinort.exe(interpreter));
325 replace_link_to_executable(targetwt.as_path(), &executable_target)
326 .map_err(Error::Python)?;
327 }
328 } else if matches!(interpreter.platform().os(), Os::Pyodide { .. }) {
329 let target = scripts.join(WindowsExecutable::Python.exe(interpreter));
332 replace_link_to_executable(target.as_path(), &executable_target)
333 .map_err(Error::Python)?;
334 } else {
335 copy_launcher_windows(
337 WindowsExecutable::Python,
338 interpreter,
339 &base_python,
340 &scripts,
341 python_home,
342 )?;
343
344 match interpreter.implementation_name() {
345 "graalpy" => {
346 copy_launcher_windows(
348 WindowsExecutable::GraalPy,
349 interpreter,
350 &base_python,
351 &scripts,
352 python_home,
353 )?;
354 copy_launcher_windows(
355 WindowsExecutable::PythonMajor,
356 interpreter,
357 &base_python,
358 &scripts,
359 python_home,
360 )?;
361 }
362 "pypy" => {
363 copy_launcher_windows(
365 WindowsExecutable::PythonMajor,
366 interpreter,
367 &base_python,
368 &scripts,
369 python_home,
370 )?;
371 copy_launcher_windows(
372 WindowsExecutable::PythonMajorMinor,
373 interpreter,
374 &base_python,
375 &scripts,
376 python_home,
377 )?;
378 copy_launcher_windows(
379 WindowsExecutable::Pythonw,
380 interpreter,
381 &base_python,
382 &scripts,
383 python_home,
384 )?;
385 copy_launcher_windows(
386 WindowsExecutable::PyPy,
387 interpreter,
388 &base_python,
389 &scripts,
390 python_home,
391 )?;
392 copy_launcher_windows(
393 WindowsExecutable::PyPyMajor,
394 interpreter,
395 &base_python,
396 &scripts,
397 python_home,
398 )?;
399 copy_launcher_windows(
400 WindowsExecutable::PyPyMajorMinor,
401 interpreter,
402 &base_python,
403 &scripts,
404 python_home,
405 )?;
406 copy_launcher_windows(
407 WindowsExecutable::PyPyw,
408 interpreter,
409 &base_python,
410 &scripts,
411 python_home,
412 )?;
413 copy_launcher_windows(
414 WindowsExecutable::PyPyMajorMinorw,
415 interpreter,
416 &base_python,
417 &scripts,
418 python_home,
419 )?;
420 }
421 _ => {
422 copy_launcher_windows(
424 WindowsExecutable::Pythonw,
425 interpreter,
426 &base_python,
427 &scripts,
428 python_home,
429 )?;
430
431 if interpreter.gil_disabled() {
433 copy_launcher_windows(
434 WindowsExecutable::PythonMajorMinort,
435 interpreter,
436 &base_python,
437 &scripts,
438 python_home,
439 )?;
440 copy_launcher_windows(
441 WindowsExecutable::PythonwMajorMinort,
442 interpreter,
443 &base_python,
444 &scripts,
445 python_home,
446 )?;
447 }
448 }
449 }
450 }
451 }
452
453 #[cfg(not(any(unix, windows)))]
454 {
455 compile_error!("Only Windows and Unix are supported")
456 }
457
458 for (name, template) in ACTIVATE_TEMPLATES {
460 if relocatable && *name == "activate.csh" {
464 continue;
465 }
466
467 let path_sep = if cfg!(windows) { ";" } else { ":" };
468
469 let relative_site_packages = [
470 interpreter.virtualenv().purelib.as_path(),
471 interpreter.virtualenv().platlib.as_path(),
472 ]
473 .iter()
474 .dedup()
475 .map(|path| {
476 pathdiff::diff_paths(path, &interpreter.virtualenv().scripts)
477 .expect("Failed to calculate relative path to site-packages")
478 })
479 .map(|path| path.simplified().to_str().unwrap().replace('\\', "\\\\"))
480 .join(path_sep);
481
482 let virtual_env_dir = match (relocatable, name.to_owned()) {
483 (true, "activate") => {
484 r#"'"$(dirname -- "$(dirname -- "$(realpath -- "$SCRIPT_PATH")")")"'"#.to_string()
485 }
486 (true, "activate.bat") => r"%~dp0..".to_string(),
487 (true, "activate.fish") => {
488 r#"'"$(dirname -- "$(cd "$(dirname -- "$(status -f)")"; and pwd)")"'"#.to_string()
489 }
490 (true, "activate.nu") => r"(path self | path dirname | path dirname)".to_string(),
491 (false, "activate.nu") => {
492 format!(
493 "'{}'",
494 escape_posix_for_single_quotes(location.simplified().to_str().unwrap())
495 )
496 }
497 _ => escape_posix_for_single_quotes(location.simplified().to_str().unwrap()),
499 };
500
501 let activator = template
502 .replace("{{ VIRTUAL_ENV_DIR }}", &virtual_env_dir)
503 .replace("{{ BIN_NAME }}", bin_name)
504 .replace(
505 "{{ VIRTUAL_PROMPT }}",
506 prompt.as_deref().unwrap_or_default(),
507 )
508 .replace("{{ PATH_SEP }}", path_sep)
509 .replace("{{ RELATIVE_SITE_PACKAGES }}", &relative_site_packages);
510 fs_err::write(scripts.join(name), activator)?;
511 }
512
513 let mut pyvenv_cfg_data: Vec<(String, String)> = vec![
514 (
515 "home".to_string(),
516 python_home.simplified_display().to_string(),
517 ),
518 (
519 "implementation".to_string(),
520 interpreter
521 .markers()
522 .platform_python_implementation()
523 .to_string(),
524 ),
525 ("uv".to_string(), version().to_string()),
526 (
527 "version_info".to_string(),
528 interpreter.markers().python_full_version().string.clone(),
529 ),
530 (
531 "include-system-site-packages".to_string(),
532 if system_site_packages {
533 "true".to_string()
534 } else {
535 "false".to_string()
536 },
537 ),
538 ];
539
540 if relocatable {
541 pyvenv_cfg_data.push(("relocatable".to_string(), "true".to_string()));
542 }
543
544 if seed {
545 pyvenv_cfg_data.push(("seed".to_string(), "true".to_string()));
546 }
547
548 if let Some(prompt) = prompt {
549 pyvenv_cfg_data.push(("prompt".to_string(), prompt));
550 }
551
552 if cfg!(windows) && interpreter.markers().implementation_name() == "graalpy" {
553 pyvenv_cfg_data.push((
554 "venvlauncher_command".to_string(),
555 python_home
556 .join("graalpy.exe")
557 .simplified_display()
558 .to_string(),
559 ));
560 }
561
562 let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?);
563 write_cfg(&mut pyvenv_cfg, &pyvenv_cfg_data)?;
564 drop(pyvenv_cfg);
565
566 let site_packages = location.join(&interpreter.virtualenv().purelib);
568 fs_err::create_dir_all(&site_packages)?;
569
570 #[cfg(unix)]
573 if interpreter.pointer_size().is_64()
574 && interpreter.markers().os_name() == "posix"
575 && interpreter.markers().sys_platform() != "darwin"
576 {
577 match fs_err::os::unix::fs::symlink("lib", location.join("lib64")) {
578 Ok(()) => {}
579 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
580 Err(err) => {
581 return Err(err.into());
582 }
583 }
584 }
585
586 fs_err::write(site_packages.join("_virtualenv.py"), VIRTUALENV_PATCH)?;
588 fs_err::write(site_packages.join("_virtualenv.pth"), "import _virtualenv")?;
589
590 Ok(VirtualEnvironment {
591 scheme: Scheme {
592 purelib: location.join(&interpreter.virtualenv().purelib),
593 platlib: location.join(&interpreter.virtualenv().platlib),
594 scripts: location.join(&interpreter.virtualenv().scripts),
595 data: location.join(&interpreter.virtualenv().data),
596 include: location.join(&interpreter.virtualenv().include),
597 },
598 root: location,
599 executable,
600 base_executable: base_python,
601 })
602}
603
604fn confirm_clear(location: &Path, name: &'static str) -> Result<Option<bool>, io::Error> {
608 let term = Term::stderr();
609 if term.is_term() {
610 let prompt = format!(
611 "A {name} already exists at `{}`. Do you want to replace it?",
612 location.user_display(),
613 );
614 let hint = format!(
615 "Use the `{}` flag or set `{}` to skip this prompt",
616 "--clear".green(),
617 "UV_VENV_CLEAR=1".green()
618 );
619 Ok(Some(uv_console::confirm_with_hint(
620 &prompt, &hint, &term, true,
621 )?))
622 } else {
623 Ok(None)
624 }
625}
626
627#[derive(Debug, Copy, Clone, Eq, PartialEq)]
628pub enum ClearNonVirtualenv {
629 Allow,
631 Warn,
633 Error,
635}
636
637#[derive(Debug, Copy, Clone, Eq, PartialEq)]
638pub enum RemovalReason {
639 UserRequest(ClearNonVirtualenv),
641 TemporaryEnvironment,
644 ManagedEnvironment,
647}
648
649impl std::fmt::Display for RemovalReason {
650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651 match self {
652 Self::UserRequest(_) => f.write_str("requested with `--clear`"),
653 Self::ManagedEnvironment => f.write_str("environment is managed by uv"),
654 Self::TemporaryEnvironment => f.write_str("environment is temporary"),
655 }
656 }
657}
658
659#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
660pub enum OnExisting {
661 #[default]
665 Prompt,
666 Fail,
668 Allow,
671 Remove(RemovalReason),
673}
674
675impl OnExisting {
676 pub fn from_args(
677 allow_existing: bool,
678 clear: bool,
679 no_clear: bool,
680 clear_non_virtualenv: ClearNonVirtualenv,
681 ) -> Self {
682 if allow_existing {
683 Self::Allow
684 } else if clear {
685 Self::Remove(RemovalReason::UserRequest(clear_non_virtualenv))
686 } else if no_clear {
687 Self::Fail
688 } else {
689 Self::Prompt
690 }
691 }
692}
693
694#[derive(Debug, Copy, Clone)]
695enum WindowsExecutable {
696 Python,
698 PythonMajor,
700 PythonMajorMinor,
702 PythonMajorMinort,
704 Pythonw,
706 PythonwMajorMinort,
708 PyPy,
710 PyPyMajor,
712 PyPyMajorMinor,
714 PyPyw,
716 PyPyMajorMinorw,
718 GraalPy,
720}
721
722impl WindowsExecutable {
723 fn exe(self, interpreter: &Interpreter) -> String {
725 match self {
726 Self::Python => String::from("python.exe"),
727 Self::PythonMajor => {
728 format!("python{}.exe", interpreter.python_major())
729 }
730 Self::PythonMajorMinor => {
731 format!(
732 "python{}.{}.exe",
733 interpreter.python_major(),
734 interpreter.python_minor()
735 )
736 }
737 Self::PythonMajorMinort => {
738 format!(
739 "python{}.{}t.exe",
740 interpreter.python_major(),
741 interpreter.python_minor()
742 )
743 }
744 Self::Pythonw => String::from("pythonw.exe"),
745 Self::PythonwMajorMinort => {
746 format!(
747 "pythonw{}.{}t.exe",
748 interpreter.python_major(),
749 interpreter.python_minor()
750 )
751 }
752 Self::PyPy => String::from("pypy.exe"),
753 Self::PyPyMajor => {
754 format!("pypy{}.exe", interpreter.python_major())
755 }
756 Self::PyPyMajorMinor => {
757 format!(
758 "pypy{}.{}.exe",
759 interpreter.python_major(),
760 interpreter.python_minor()
761 )
762 }
763 Self::PyPyw => String::from("pypyw.exe"),
764 Self::PyPyMajorMinorw => {
765 format!(
766 "pypy{}.{}w.exe",
767 interpreter.python_major(),
768 interpreter.python_minor()
769 )
770 }
771 Self::GraalPy => String::from("graalpy.exe"),
772 }
773 }
774
775 fn launcher(self, interpreter: &Interpreter) -> &'static str {
777 match self {
778 Self::Python | Self::PythonMajor | Self::PythonMajorMinor
779 if interpreter.gil_disabled() =>
780 {
781 "venvlaunchert.exe"
782 }
783 Self::Python | Self::PythonMajor | Self::PythonMajorMinor => "venvlauncher.exe",
784 Self::Pythonw if interpreter.gil_disabled() => "venvwlaunchert.exe",
785 Self::Pythonw => "venvwlauncher.exe",
786 Self::PythonMajorMinort => "venvlaunchert.exe",
787 Self::PythonwMajorMinort => "venvwlaunchert.exe",
788 Self::PyPy | Self::PyPyMajor | Self::PyPyMajorMinor => "venvlauncher.exe",
791 Self::PyPyw | Self::PyPyMajorMinorw => "venvwlauncher.exe",
792 Self::GraalPy => "venvlauncher.exe",
793 }
794 }
795}
796
797fn copy_launcher_windows(
803 executable: WindowsExecutable,
804 interpreter: &Interpreter,
805 base_python: &Path,
806 scripts: &Path,
807 python_home: &Path,
808) -> Result<(), Error> {
809 let shim = interpreter
811 .stdlib()
812 .join("venv")
813 .join("scripts")
814 .join("nt")
815 .join(executable.exe(interpreter));
816 match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
817 Ok(_) => return Ok(()),
818 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
819 Err(err) => {
820 return Err(err.into());
821 }
822 }
823
824 let shim = interpreter
828 .stdlib()
829 .join("venv")
830 .join("scripts")
831 .join("nt")
832 .join(executable.launcher(interpreter));
833 match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
834 Ok(_) => return Ok(()),
835 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
836 Err(err) => {
837 return Err(err.into());
838 }
839 }
840
841 let shim = base_python.with_file_name(executable.launcher(interpreter));
844 match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
845 Ok(_) => return Ok(()),
846 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
847 Err(err) => {
848 return Err(err.into());
849 }
850 }
851
852 match fs_err::copy(
856 base_python.with_file_name(executable.exe(interpreter)),
857 scripts.join(executable.exe(interpreter)),
858 ) {
859 Ok(_) => {
860 for directory in [
863 python_home,
864 interpreter.sys_base_prefix().join("DLLs").as_path(),
865 ] {
866 let entries = match fs_err::read_dir(directory) {
867 Ok(read_dir) => read_dir,
868 Err(err) if err.kind() == io::ErrorKind::NotFound => {
869 continue;
870 }
871 Err(err) => {
872 return Err(err.into());
873 }
874 };
875 for entry in entries {
876 let entry = entry?;
877 let path = entry.path();
878 if path.extension().is_some_and(|ext| {
879 ext.eq_ignore_ascii_case("dll") || ext.eq_ignore_ascii_case("pyd")
880 }) {
881 if let Some(file_name) = path.file_name() {
882 fs_err::copy(&path, scripts.join(file_name))?;
883 }
884 }
885 }
886 }
887
888 match fs_err::read_dir(python_home) {
890 Ok(entries) => {
891 for entry in entries {
892 let entry = entry?;
893 let path = entry.path();
894 if path
895 .extension()
896 .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
897 {
898 if let Some(file_name) = path.file_name() {
899 fs_err::copy(&path, scripts.join(file_name))?;
900 }
901 }
902 }
903 }
904 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
905 Err(err) => {
906 return Err(err.into());
907 }
908 }
909
910 return Ok(());
911 }
912 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
913 Err(err) => {
914 return Err(err.into());
915 }
916 }
917
918 Err(Error::NotFound(base_python.user_display().to_string()))
919}