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!(
329 interpreter.platform().os(),
330 Os::Pyodide { .. } | Os::PyEmscripten { .. }
331 ) {
332 let target = scripts.join(WindowsExecutable::Python.exe(interpreter));
335 replace_link_to_executable(target.as_path(), &executable_target)
336 .map_err(Error::Python)?;
337 } else {
338 copy_launcher_windows(
340 WindowsExecutable::Python,
341 interpreter,
342 &base_python,
343 &scripts,
344 python_home,
345 )?;
346
347 match interpreter.implementation_name() {
348 "graalpy" => {
349 copy_launcher_windows(
351 WindowsExecutable::GraalPy,
352 interpreter,
353 &base_python,
354 &scripts,
355 python_home,
356 )?;
357 copy_launcher_windows(
358 WindowsExecutable::PythonMajor,
359 interpreter,
360 &base_python,
361 &scripts,
362 python_home,
363 )?;
364 }
365 "pypy" => {
366 copy_launcher_windows(
368 WindowsExecutable::PythonMajor,
369 interpreter,
370 &base_python,
371 &scripts,
372 python_home,
373 )?;
374 copy_launcher_windows(
375 WindowsExecutable::PythonMajorMinor,
376 interpreter,
377 &base_python,
378 &scripts,
379 python_home,
380 )?;
381 copy_launcher_windows(
382 WindowsExecutable::Pythonw,
383 interpreter,
384 &base_python,
385 &scripts,
386 python_home,
387 )?;
388 copy_launcher_windows(
389 WindowsExecutable::PyPy,
390 interpreter,
391 &base_python,
392 &scripts,
393 python_home,
394 )?;
395 copy_launcher_windows(
396 WindowsExecutable::PyPyMajor,
397 interpreter,
398 &base_python,
399 &scripts,
400 python_home,
401 )?;
402 copy_launcher_windows(
403 WindowsExecutable::PyPyMajorMinor,
404 interpreter,
405 &base_python,
406 &scripts,
407 python_home,
408 )?;
409 copy_launcher_windows(
410 WindowsExecutable::PyPyw,
411 interpreter,
412 &base_python,
413 &scripts,
414 python_home,
415 )?;
416 copy_launcher_windows(
417 WindowsExecutable::PyPyMajorMinorw,
418 interpreter,
419 &base_python,
420 &scripts,
421 python_home,
422 )?;
423 }
424 _ => {
425 copy_launcher_windows(
427 WindowsExecutable::Pythonw,
428 interpreter,
429 &base_python,
430 &scripts,
431 python_home,
432 )?;
433
434 if interpreter.gil_disabled() {
436 copy_launcher_windows(
437 WindowsExecutable::PythonMajorMinort,
438 interpreter,
439 &base_python,
440 &scripts,
441 python_home,
442 )?;
443 copy_launcher_windows(
444 WindowsExecutable::PythonwMajorMinort,
445 interpreter,
446 &base_python,
447 &scripts,
448 python_home,
449 )?;
450 }
451 }
452 }
453 }
454 }
455
456 #[cfg(not(any(unix, windows)))]
457 {
458 compile_error!("Only Windows and Unix are supported")
459 }
460
461 for (name, template) in ACTIVATE_TEMPLATES {
463 if relocatable && *name == "activate.csh" {
467 continue;
468 }
469
470 let path_sep = if cfg!(windows) { ";" } else { ":" };
471
472 let relative_site_packages = [
473 interpreter.virtualenv().purelib.as_path(),
474 interpreter.virtualenv().platlib.as_path(),
475 ]
476 .iter()
477 .dedup()
478 .map(|path| {
479 pathdiff::diff_paths(path, &interpreter.virtualenv().scripts)
480 .expect("Failed to calculate relative path to site-packages")
481 })
482 .map(|path| path.simplified().to_str().unwrap().replace('\\', "\\\\"))
483 .join(path_sep);
484
485 let virtual_env_dir = match (relocatable, name.to_owned()) {
486 (true, "activate") => {
487 r#"'"$(dirname -- "$(dirname -- "$(realpath -- "$SCRIPT_PATH")")")"'"#.to_string()
488 }
489 (true, "activate.bat") => r"%~dp0..".to_string(),
490 (true, "activate.fish") => {
491 r#"'"$(dirname -- "$(cd "$(dirname -- "$(status -f)")"; and pwd)")"'"#.to_string()
492 }
493 (true, "activate.nu") => r"(path self | path dirname | path dirname)".to_string(),
494 (false, "activate.nu") => {
495 format!(
496 "'{}'",
497 escape_posix_for_single_quotes(location.simplified().to_str().unwrap())
498 )
499 }
500 _ => escape_posix_for_single_quotes(location.simplified().to_str().unwrap()),
502 };
503
504 let activator = template
505 .replace("{{ VIRTUAL_ENV_DIR }}", &virtual_env_dir)
506 .replace("{{ BIN_NAME }}", bin_name)
507 .replace(
508 "{{ VIRTUAL_PROMPT }}",
509 prompt.as_deref().unwrap_or_default(),
510 )
511 .replace("{{ PATH_SEP }}", path_sep)
512 .replace("{{ RELATIVE_SITE_PACKAGES }}", &relative_site_packages);
513 fs_err::write(scripts.join(name), activator)?;
514 }
515
516 let mut pyvenv_cfg_data: Vec<(String, String)> = vec![
517 (
518 "home".to_string(),
519 python_home.simplified_display().to_string(),
520 ),
521 (
522 "implementation".to_string(),
523 interpreter
524 .markers()
525 .platform_python_implementation()
526 .to_string(),
527 ),
528 ("uv".to_string(), version().to_string()),
529 (
530 "version_info".to_string(),
531 interpreter.markers().python_full_version().string.clone(),
532 ),
533 (
534 "include-system-site-packages".to_string(),
535 if system_site_packages {
536 "true".to_string()
537 } else {
538 "false".to_string()
539 },
540 ),
541 ];
542
543 if relocatable {
544 pyvenv_cfg_data.push(("relocatable".to_string(), "true".to_string()));
545 }
546
547 if seed {
548 pyvenv_cfg_data.push(("seed".to_string(), "true".to_string()));
549 }
550
551 if let Some(prompt) = prompt {
552 pyvenv_cfg_data.push(("prompt".to_string(), prompt));
553 }
554
555 if cfg!(windows) && interpreter.markers().implementation_name() == "graalpy" {
556 pyvenv_cfg_data.push((
557 "venvlauncher_command".to_string(),
558 python_home
559 .join("graalpy.exe")
560 .simplified_display()
561 .to_string(),
562 ));
563 }
564
565 let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?);
566 write_cfg(&mut pyvenv_cfg, &pyvenv_cfg_data)?;
567 drop(pyvenv_cfg);
568
569 let site_packages = location.join(&interpreter.virtualenv().purelib);
571 fs_err::create_dir_all(&site_packages)?;
572
573 #[cfg(unix)]
576 if interpreter.pointer_size().is_64()
577 && interpreter.markers().os_name() == "posix"
578 && interpreter.markers().sys_platform() != "darwin"
579 {
580 match fs_err::os::unix::fs::symlink("lib", location.join("lib64")) {
581 Ok(()) => {}
582 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
583 Err(err) => {
584 return Err(err.into());
585 }
586 }
587 }
588
589 fs_err::write(site_packages.join("_virtualenv.py"), VIRTUALENV_PATCH)?;
591 fs_err::write(site_packages.join("_virtualenv.pth"), "import _virtualenv")?;
592
593 Ok(VirtualEnvironment {
594 scheme: Scheme {
595 purelib: location.join(&interpreter.virtualenv().purelib),
596 platlib: location.join(&interpreter.virtualenv().platlib),
597 scripts: location.join(&interpreter.virtualenv().scripts),
598 data: location.join(&interpreter.virtualenv().data),
599 include: location.join(&interpreter.virtualenv().include),
600 },
601 root: location,
602 executable,
603 base_executable: base_python,
604 })
605}
606
607fn confirm_clear(location: &Path, name: &'static str) -> Result<Option<bool>, io::Error> {
611 let term = Term::stderr();
612 if term.is_term() {
613 let prompt = format!(
614 "A {name} already exists at `{}`. Do you want to replace it?",
615 location.user_display(),
616 );
617 let hint = format!(
618 "Use the `{}` flag or set `{}` to skip this prompt",
619 "--clear".green(),
620 "UV_VENV_CLEAR=1".green()
621 );
622 Ok(Some(uv_console::confirm_with_hint(
623 &prompt, &hint, &term, true,
624 )?))
625 } else {
626 Ok(None)
627 }
628}
629
630#[derive(Debug, Copy, Clone, Eq, PartialEq)]
631pub enum ClearNonVirtualenv {
632 Allow,
634 Warn,
636 Error,
638}
639
640#[derive(Debug, Copy, Clone, Eq, PartialEq)]
641pub enum RemovalReason {
642 UserRequest(ClearNonVirtualenv),
644 TemporaryEnvironment,
647 ManagedEnvironment,
650}
651
652impl std::fmt::Display for RemovalReason {
653 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
654 match self {
655 Self::UserRequest(_) => f.write_str("requested with `--clear`"),
656 Self::ManagedEnvironment => f.write_str("environment is managed by uv"),
657 Self::TemporaryEnvironment => f.write_str("environment is temporary"),
658 }
659 }
660}
661
662#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
663pub enum OnExisting {
664 #[default]
668 Prompt,
669 Fail,
671 Allow,
674 Remove(RemovalReason),
676}
677
678impl OnExisting {
679 pub fn from_args(
680 allow_existing: bool,
681 clear: bool,
682 no_clear: bool,
683 clear_non_virtualenv: ClearNonVirtualenv,
684 ) -> Self {
685 if allow_existing {
686 Self::Allow
687 } else if clear {
688 Self::Remove(RemovalReason::UserRequest(clear_non_virtualenv))
689 } else if no_clear {
690 Self::Fail
691 } else {
692 Self::Prompt
693 }
694 }
695}
696
697#[derive(Debug, Copy, Clone)]
698enum WindowsExecutable {
699 Python,
701 PythonMajor,
703 PythonMajorMinor,
705 PythonMajorMinort,
707 Pythonw,
709 PythonwMajorMinort,
711 PyPy,
713 PyPyMajor,
715 PyPyMajorMinor,
717 PyPyw,
719 PyPyMajorMinorw,
721 GraalPy,
723}
724
725impl WindowsExecutable {
726 fn exe(self, interpreter: &Interpreter) -> String {
728 match self {
729 Self::Python => String::from("python.exe"),
730 Self::PythonMajor => {
731 format!("python{}.exe", interpreter.python_major())
732 }
733 Self::PythonMajorMinor => {
734 format!(
735 "python{}.{}.exe",
736 interpreter.python_major(),
737 interpreter.python_minor()
738 )
739 }
740 Self::PythonMajorMinort => {
741 format!(
742 "python{}.{}t.exe",
743 interpreter.python_major(),
744 interpreter.python_minor()
745 )
746 }
747 Self::Pythonw => String::from("pythonw.exe"),
748 Self::PythonwMajorMinort => {
749 format!(
750 "pythonw{}.{}t.exe",
751 interpreter.python_major(),
752 interpreter.python_minor()
753 )
754 }
755 Self::PyPy => String::from("pypy.exe"),
756 Self::PyPyMajor => {
757 format!("pypy{}.exe", interpreter.python_major())
758 }
759 Self::PyPyMajorMinor => {
760 format!(
761 "pypy{}.{}.exe",
762 interpreter.python_major(),
763 interpreter.python_minor()
764 )
765 }
766 Self::PyPyw => String::from("pypyw.exe"),
767 Self::PyPyMajorMinorw => {
768 format!(
769 "pypy{}.{}w.exe",
770 interpreter.python_major(),
771 interpreter.python_minor()
772 )
773 }
774 Self::GraalPy => String::from("graalpy.exe"),
775 }
776 }
777
778 fn launcher(self, interpreter: &Interpreter) -> &'static str {
780 match self {
781 Self::Python | Self::PythonMajor | Self::PythonMajorMinor
782 if interpreter.gil_disabled() =>
783 {
784 "venvlaunchert.exe"
785 }
786 Self::Python | Self::PythonMajor | Self::PythonMajorMinor => "venvlauncher.exe",
787 Self::Pythonw if interpreter.gil_disabled() => "venvwlaunchert.exe",
788 Self::Pythonw => "venvwlauncher.exe",
789 Self::PythonMajorMinort => "venvlaunchert.exe",
790 Self::PythonwMajorMinort => "venvwlaunchert.exe",
791 Self::PyPy | Self::PyPyMajor | Self::PyPyMajorMinor => "venvlauncher.exe",
794 Self::PyPyw | Self::PyPyMajorMinorw => "venvwlauncher.exe",
795 Self::GraalPy => "venvlauncher.exe",
796 }
797 }
798}
799
800fn copy_launcher_windows(
806 executable: WindowsExecutable,
807 interpreter: &Interpreter,
808 base_python: &Path,
809 scripts: &Path,
810 python_home: &Path,
811) -> Result<(), Error> {
812 let shim = interpreter
814 .stdlib()
815 .join("venv")
816 .join("scripts")
817 .join("nt")
818 .join(executable.exe(interpreter));
819 match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
820 Ok(_) => return Ok(()),
821 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
822 Err(err) => {
823 return Err(err.into());
824 }
825 }
826
827 let shim = interpreter
831 .stdlib()
832 .join("venv")
833 .join("scripts")
834 .join("nt")
835 .join(executable.launcher(interpreter));
836 match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
837 Ok(_) => return Ok(()),
838 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
839 Err(err) => {
840 return Err(err.into());
841 }
842 }
843
844 let shim = base_python.with_file_name(executable.launcher(interpreter));
847 match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
848 Ok(_) => return Ok(()),
849 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
850 Err(err) => {
851 return Err(err.into());
852 }
853 }
854
855 match fs_err::copy(
859 base_python.with_file_name(executable.exe(interpreter)),
860 scripts.join(executable.exe(interpreter)),
861 ) {
862 Ok(_) => {
863 for directory in [
866 python_home,
867 interpreter.sys_base_prefix().join("DLLs").as_path(),
868 ] {
869 let entries = match fs_err::read_dir(directory) {
870 Ok(read_dir) => read_dir,
871 Err(err) if err.kind() == io::ErrorKind::NotFound => {
872 continue;
873 }
874 Err(err) => {
875 return Err(err.into());
876 }
877 };
878 for entry in entries {
879 let entry = entry?;
880 let path = entry.path();
881 if path.extension().is_some_and(|ext| {
882 ext.eq_ignore_ascii_case("dll") || ext.eq_ignore_ascii_case("pyd")
883 }) {
884 if let Some(file_name) = path.file_name() {
885 fs_err::copy(&path, scripts.join(file_name))?;
886 }
887 }
888 }
889 }
890
891 match fs_err::read_dir(python_home) {
893 Ok(entries) => {
894 for entry in entries {
895 let entry = entry?;
896 let path = entry.path();
897 if path
898 .extension()
899 .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
900 {
901 if let Some(file_name) = path.file_name() {
902 fs_err::copy(&path, scripts.join(file_name))?;
903 }
904 }
905 }
906 }
907 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
908 Err(err) => {
909 return Err(err.into());
910 }
911 }
912
913 return Ok(());
914 }
915 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
916 Err(err) => {
917 return Err(err.into());
918 }
919 }
920
921 Err(Error::NotFound(base_python.user_display().to_string()))
922}