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 uv_fs::clear_virtualenv(location)?;
154 }
155 OnExisting::Fail => return err,
156 OnExisting::Prompt if !is_virtualenv => return err,
158 OnExisting::Prompt => {
159 match confirm_clear(location, name)? {
160 Some(true) => {
161 debug!("Removing existing {name} due to confirmation");
162 uv_fs::clear_virtualenv(location)?;
163 }
164 Some(false) => return err,
165 None => {
167 return Err(Error::Exists {
168 name,
169 path: location.to_path_buf(),
170 });
171 }
172 }
173 }
174 }
175 }
176 Ok(_) => {
177 return Err(Error::Io(io::Error::new(
179 io::ErrorKind::AlreadyExists,
180 format!("Object already exists at `{}`", location.user_display()),
181 )));
182 }
183 Err(err) if err.kind() == io::ErrorKind::NotFound => {
184 fs_err::create_dir_all(location)?;
185 }
186 Err(err) => return Err(Error::Io(err)),
187 }
188
189 let location = absolute;
191
192 let bin_name = if cfg!(unix) {
193 "bin"
194 } else if cfg!(windows) {
195 "Scripts"
196 } else {
197 unimplemented!("Only Windows and Unix are supported")
198 };
199 let scripts = location.join(&interpreter.virtualenv().scripts);
200
201 cachedir::ensure_tag(&location)?;
203
204 fs_err::write(location.join(".gitignore"), "*")?;
206
207 let mut using_minor_version_link = false;
208 let executable_target = if upgradeable {
209 if let Some(minor_version_link) =
210 ManagedPythonInstallation::try_from_interpreter(interpreter)
211 .and_then(|installation| PythonMinorVersionLink::from_installation(&installation))
212 {
213 if !minor_version_link.exists() {
214 base_python.clone()
215 } else {
216 let debug_symlink_term = if cfg!(windows) {
217 "junction"
218 } else {
219 "symlink directory"
220 };
221 debug!(
222 "Using {} {} instead of base Python path: {}",
223 debug_symlink_term,
224 &minor_version_link.symlink_directory.display(),
225 &base_python.display()
226 );
227 using_minor_version_link = true;
228 minor_version_link.symlink_executable.clone()
229 }
230 } else {
231 base_python.clone()
232 }
233 } else {
234 base_python.clone()
235 };
236
237 let python_home = executable_target
242 .parent()
243 .ok_or_else(|| {
244 io::Error::new(
245 io::ErrorKind::NotFound,
246 "The Python interpreter needs to have a parent directory",
247 )
248 })?
249 .to_path_buf();
250 let python_home = python_home.as_path();
251
252 fs_err::create_dir_all(&scripts)?;
254 let executable = scripts.join(format!("python{EXE_SUFFIX}"));
255
256 #[cfg(unix)]
257 {
258 uv_fs::replace_symlink(&executable_target, &executable)?;
259 uv_fs::replace_symlink(
260 "python",
261 scripts.join(format!("python{}", interpreter.python_major())),
262 )?;
263 uv_fs::replace_symlink(
264 "python",
265 scripts.join(format!(
266 "python{}.{}",
267 interpreter.python_major(),
268 interpreter.python_minor(),
269 )),
270 )?;
271 if interpreter.gil_disabled() {
272 uv_fs::replace_symlink(
273 "python",
274 scripts.join(format!(
275 "python{}.{}t",
276 interpreter.python_major(),
277 interpreter.python_minor(),
278 )),
279 )?;
280 }
281
282 if interpreter.markers().implementation_name() == "pypy" {
283 uv_fs::replace_symlink(
284 "python",
285 scripts.join(format!("pypy{}", interpreter.python_major())),
286 )?;
287 uv_fs::replace_symlink("python", scripts.join("pypy"))?;
288 }
289
290 if interpreter.markers().implementation_name() == "graalpy" {
291 uv_fs::replace_symlink("python", scripts.join("graalpy"))?;
292 }
293 }
294
295 if cfg!(windows) {
299 if using_minor_version_link {
300 let target = scripts.join(WindowsExecutable::Python.exe(interpreter));
301 replace_link_to_executable(target.as_path(), &executable_target)
302 .map_err(Error::Python)?;
303 let targetw = scripts.join(WindowsExecutable::Pythonw.exe(interpreter));
304 replace_link_to_executable(targetw.as_path(), &executable_target)
305 .map_err(Error::Python)?;
306 if interpreter.gil_disabled() {
307 let targett = scripts.join(WindowsExecutable::PythonMajorMinort.exe(interpreter));
308 replace_link_to_executable(targett.as_path(), &executable_target)
309 .map_err(Error::Python)?;
310 let targetwt = scripts.join(WindowsExecutable::PythonwMajorMinort.exe(interpreter));
311 replace_link_to_executable(targetwt.as_path(), &executable_target)
312 .map_err(Error::Python)?;
313 }
314 } else if matches!(
315 interpreter.platform().os(),
316 Os::Pyodide { .. } | Os::PyEmscripten { .. }
317 ) {
318 let target = scripts.join(WindowsExecutable::Python.exe(interpreter));
321 replace_link_to_executable(target.as_path(), &executable_target)
322 .map_err(Error::Python)?;
323 } else {
324 copy_launcher_windows(
326 WindowsExecutable::Python,
327 interpreter,
328 &base_python,
329 &scripts,
330 python_home,
331 )?;
332
333 match interpreter.implementation_name() {
334 "graalpy" => {
335 copy_launcher_windows(
337 WindowsExecutable::GraalPy,
338 interpreter,
339 &base_python,
340 &scripts,
341 python_home,
342 )?;
343 copy_launcher_windows(
344 WindowsExecutable::PythonMajor,
345 interpreter,
346 &base_python,
347 &scripts,
348 python_home,
349 )?;
350 }
351 "pypy" => {
352 copy_launcher_windows(
354 WindowsExecutable::PythonMajor,
355 interpreter,
356 &base_python,
357 &scripts,
358 python_home,
359 )?;
360 copy_launcher_windows(
361 WindowsExecutable::PythonMajorMinor,
362 interpreter,
363 &base_python,
364 &scripts,
365 python_home,
366 )?;
367 copy_launcher_windows(
368 WindowsExecutable::Pythonw,
369 interpreter,
370 &base_python,
371 &scripts,
372 python_home,
373 )?;
374 copy_launcher_windows(
375 WindowsExecutable::PyPy,
376 interpreter,
377 &base_python,
378 &scripts,
379 python_home,
380 )?;
381 copy_launcher_windows(
382 WindowsExecutable::PyPyMajor,
383 interpreter,
384 &base_python,
385 &scripts,
386 python_home,
387 )?;
388 copy_launcher_windows(
389 WindowsExecutable::PyPyMajorMinor,
390 interpreter,
391 &base_python,
392 &scripts,
393 python_home,
394 )?;
395 copy_launcher_windows(
396 WindowsExecutable::PyPyw,
397 interpreter,
398 &base_python,
399 &scripts,
400 python_home,
401 )?;
402 copy_launcher_windows(
403 WindowsExecutable::PyPyMajorMinorw,
404 interpreter,
405 &base_python,
406 &scripts,
407 python_home,
408 )?;
409 }
410 _ => {
411 copy_launcher_windows(
413 WindowsExecutable::Pythonw,
414 interpreter,
415 &base_python,
416 &scripts,
417 python_home,
418 )?;
419
420 if interpreter.gil_disabled() {
422 copy_launcher_windows(
423 WindowsExecutable::PythonMajorMinort,
424 interpreter,
425 &base_python,
426 &scripts,
427 python_home,
428 )?;
429 copy_launcher_windows(
430 WindowsExecutable::PythonwMajorMinort,
431 interpreter,
432 &base_python,
433 &scripts,
434 python_home,
435 )?;
436 }
437 }
438 }
439 }
440 }
441
442 #[cfg(not(any(unix, windows)))]
443 {
444 compile_error!("Only Windows and Unix are supported")
445 }
446
447 for (name, template) in ACTIVATE_TEMPLATES {
449 if relocatable && *name == "activate.csh" {
453 continue;
454 }
455
456 let path_sep = if cfg!(windows) { ";" } else { ":" };
457
458 let relative_site_packages = [
459 interpreter.virtualenv().purelib.as_path(),
460 interpreter.virtualenv().platlib.as_path(),
461 ]
462 .iter()
463 .dedup()
464 .map(|path| {
465 pathdiff::diff_paths(path, &interpreter.virtualenv().scripts)
466 .expect("Failed to calculate relative path to site-packages")
467 })
468 .map(|path| path.simplified().to_str().unwrap().replace('\\', "\\\\"))
469 .join(path_sep);
470
471 let virtual_env_dir = match (relocatable, name.to_owned()) {
472 (true, "activate") => {
473 r#"'"$(dirname -- "$(dirname -- "$(realpath -- "$SCRIPT_PATH")")")"'"#.to_string()
474 }
475 (true, "activate.bat") => r"%~dp0..".to_string(),
476 (true, "activate.fish") => {
477 r"'(dirname -- (dirname -- (realpath -- (status -f))))'".to_string()
478 }
479 (true, "activate.nu") => r"(path self | path dirname | path dirname)".to_string(),
480 (false, "activate.nu") => {
481 format!(
482 "'{}'",
483 escape_posix_for_single_quotes(location.simplified().to_str().unwrap())
484 )
485 }
486 _ => escape_posix_for_single_quotes(location.simplified().to_str().unwrap()),
488 };
489
490 let activator = template
491 .replace("{{ VIRTUAL_ENV_DIR }}", &virtual_env_dir)
492 .replace("{{ BIN_NAME }}", bin_name)
493 .replace(
494 "{{ VIRTUAL_PROMPT }}",
495 prompt.as_deref().unwrap_or_default(),
496 )
497 .replace("{{ PATH_SEP }}", path_sep)
498 .replace("{{ RELATIVE_SITE_PACKAGES }}", &relative_site_packages);
499 fs_err::write(scripts.join(name), activator)?;
500 }
501
502 let mut pyvenv_cfg_data: Vec<(String, String)> = vec![
503 (
504 "home".to_string(),
505 python_home.simplified_display().to_string(),
506 ),
507 (
508 "implementation".to_string(),
509 interpreter
510 .markers()
511 .platform_python_implementation()
512 .to_string(),
513 ),
514 ("uv".to_string(), version().to_string()),
515 (
516 "version_info".to_string(),
517 if using_minor_version_link {
518 interpreter.python_minor_version().to_string()
519 } else {
520 interpreter.markers().python_full_version().string.clone()
521 },
522 ),
523 (
524 "include-system-site-packages".to_string(),
525 if system_site_packages {
526 "true".to_string()
527 } else {
528 "false".to_string()
529 },
530 ),
531 ];
532
533 if relocatable {
534 pyvenv_cfg_data.push(("relocatable".to_string(), "true".to_string()));
535 }
536
537 if seed {
538 pyvenv_cfg_data.push(("seed".to_string(), "true".to_string()));
539 }
540
541 if let Some(prompt) = prompt {
542 pyvenv_cfg_data.push(("prompt".to_string(), prompt));
543 }
544
545 if cfg!(windows) && interpreter.markers().implementation_name() == "graalpy" {
546 pyvenv_cfg_data.push((
547 "venvlauncher_command".to_string(),
548 python_home
549 .join("graalpy.exe")
550 .simplified_display()
551 .to_string(),
552 ));
553 }
554
555 let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?);
556 write_cfg(&mut pyvenv_cfg, &pyvenv_cfg_data)?;
557 drop(pyvenv_cfg);
558
559 let site_packages = location.join(&interpreter.virtualenv().purelib);
561 fs_err::create_dir_all(&site_packages)?;
562
563 #[cfg(unix)]
566 if interpreter.pointer_size().is_64()
567 && interpreter.markers().os_name() == "posix"
568 && interpreter.markers().sys_platform() != "darwin"
569 {
570 match fs_err::os::unix::fs::symlink("lib", location.join("lib64")) {
571 Ok(()) => {}
572 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
573 Err(err) => {
574 return Err(err.into());
575 }
576 }
577 }
578
579 fs_err::write(site_packages.join("_virtualenv.py"), VIRTUALENV_PATCH)?;
581 fs_err::write(site_packages.join("_virtualenv.pth"), "import _virtualenv")?;
582
583 Ok(VirtualEnvironment {
584 scheme: Scheme {
585 purelib: location.join(&interpreter.virtualenv().purelib),
586 platlib: location.join(&interpreter.virtualenv().platlib),
587 scripts: location.join(&interpreter.virtualenv().scripts),
588 data: location.join(&interpreter.virtualenv().data),
589 include: location.join(&interpreter.virtualenv().include),
590 },
591 root: location,
592 executable,
593 base_executable: base_python,
594 })
595}
596
597fn confirm_clear(location: &Path, name: &'static str) -> Result<Option<bool>, io::Error> {
601 let term = Term::stderr();
602 if term.is_term() {
603 let prompt = format!(
604 "A {name} already exists at `{}`. Do you want to replace it?",
605 location.user_display(),
606 );
607 let hint = format!(
608 "Use the `{}` flag or set `{}` to skip this prompt",
609 "--clear".green(),
610 "UV_VENV_CLEAR=1".green()
611 );
612 Ok(Some(uv_console::confirm_with_hint(
613 &prompt, &hint, &term, true,
614 )?))
615 } else {
616 Ok(None)
617 }
618}
619
620#[derive(Debug, Copy, Clone, Eq, PartialEq)]
621pub enum ClearNonVirtualenv {
622 Allow,
624 Warn,
626 Error,
628}
629
630#[derive(Debug, Copy, Clone, Eq, PartialEq)]
631pub enum RemovalReason {
632 UserRequest(ClearNonVirtualenv),
634 TemporaryEnvironment,
637 ManagedEnvironment,
640}
641
642impl std::fmt::Display for RemovalReason {
643 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644 match self {
645 Self::UserRequest(_) => f.write_str("requested with `--clear`"),
646 Self::ManagedEnvironment => f.write_str("environment is managed by uv"),
647 Self::TemporaryEnvironment => f.write_str("environment is temporary"),
648 }
649 }
650}
651
652#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
653pub enum OnExisting {
654 #[default]
658 Prompt,
659 Fail,
661 Allow,
664 Remove(RemovalReason),
666}
667
668impl OnExisting {
669 pub fn from_args(
670 allow_existing: bool,
671 clear: bool,
672 no_clear: bool,
673 clear_non_virtualenv: ClearNonVirtualenv,
674 ) -> Self {
675 if allow_existing {
676 Self::Allow
677 } else if clear {
678 Self::Remove(RemovalReason::UserRequest(clear_non_virtualenv))
679 } else if no_clear {
680 Self::Fail
681 } else {
682 Self::Prompt
683 }
684 }
685}
686
687#[derive(Debug, Copy, Clone)]
688enum WindowsExecutable {
689 Python,
691 PythonMajor,
693 PythonMajorMinor,
695 PythonMajorMinort,
697 Pythonw,
699 PythonwMajorMinort,
701 PyPy,
703 PyPyMajor,
705 PyPyMajorMinor,
707 PyPyw,
709 PyPyMajorMinorw,
711 GraalPy,
713}
714
715impl WindowsExecutable {
716 fn exe(self, interpreter: &Interpreter) -> String {
718 match self {
719 Self::Python => String::from("python.exe"),
720 Self::PythonMajor => {
721 format!("python{}.exe", interpreter.python_major())
722 }
723 Self::PythonMajorMinor => {
724 format!(
725 "python{}.{}.exe",
726 interpreter.python_major(),
727 interpreter.python_minor()
728 )
729 }
730 Self::PythonMajorMinort => {
731 format!(
732 "python{}.{}t.exe",
733 interpreter.python_major(),
734 interpreter.python_minor()
735 )
736 }
737 Self::Pythonw => String::from("pythonw.exe"),
738 Self::PythonwMajorMinort => {
739 format!(
740 "pythonw{}.{}t.exe",
741 interpreter.python_major(),
742 interpreter.python_minor()
743 )
744 }
745 Self::PyPy => String::from("pypy.exe"),
746 Self::PyPyMajor => {
747 format!("pypy{}.exe", interpreter.python_major())
748 }
749 Self::PyPyMajorMinor => {
750 format!(
751 "pypy{}.{}.exe",
752 interpreter.python_major(),
753 interpreter.python_minor()
754 )
755 }
756 Self::PyPyw => String::from("pypyw.exe"),
757 Self::PyPyMajorMinorw => {
758 format!(
759 "pypy{}.{}w.exe",
760 interpreter.python_major(),
761 interpreter.python_minor()
762 )
763 }
764 Self::GraalPy => String::from("graalpy.exe"),
765 }
766 }
767
768 fn launcher(self, interpreter: &Interpreter) -> &'static str {
770 match self {
771 Self::Python | Self::PythonMajor | Self::PythonMajorMinor
772 if interpreter.gil_disabled() =>
773 {
774 "venvlaunchert.exe"
775 }
776 Self::Python | Self::PythonMajor | Self::PythonMajorMinor => "venvlauncher.exe",
777 Self::Pythonw if interpreter.gil_disabled() => "venvwlaunchert.exe",
778 Self::Pythonw => "venvwlauncher.exe",
779 Self::PythonMajorMinort => "venvlaunchert.exe",
780 Self::PythonwMajorMinort => "venvwlaunchert.exe",
781 Self::PyPy | Self::PyPyMajor | Self::PyPyMajorMinor => "venvlauncher.exe",
784 Self::PyPyw | Self::PyPyMajorMinorw => "venvwlauncher.exe",
785 Self::GraalPy => "venvlauncher.exe",
786 }
787 }
788}
789
790fn copy_launcher_windows(
796 executable: WindowsExecutable,
797 interpreter: &Interpreter,
798 base_python: &Path,
799 scripts: &Path,
800 python_home: &Path,
801) -> Result<(), Error> {
802 let shim = interpreter
804 .stdlib()
805 .join("venv")
806 .join("scripts")
807 .join("nt")
808 .join(executable.exe(interpreter));
809 match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
810 Ok(_) => return Ok(()),
811 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
812 Err(err) => {
813 return Err(err.into());
814 }
815 }
816
817 let shim = interpreter
821 .stdlib()
822 .join("venv")
823 .join("scripts")
824 .join("nt")
825 .join(executable.launcher(interpreter));
826 match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
827 Ok(_) => return Ok(()),
828 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
829 Err(err) => {
830 return Err(err.into());
831 }
832 }
833
834 let shim = base_python.with_file_name(executable.launcher(interpreter));
837 match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
838 Ok(_) => return Ok(()),
839 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
840 Err(err) => {
841 return Err(err.into());
842 }
843 }
844
845 match fs_err::copy(
849 base_python.with_file_name(executable.exe(interpreter)),
850 scripts.join(executable.exe(interpreter)),
851 ) {
852 Ok(_) => {
853 for directory in [
856 python_home,
857 interpreter.sys_base_prefix().join("DLLs").as_path(),
858 ] {
859 let entries = match fs_err::read_dir(directory) {
860 Ok(read_dir) => read_dir,
861 Err(err) if err.kind() == io::ErrorKind::NotFound => {
862 continue;
863 }
864 Err(err) => {
865 return Err(err.into());
866 }
867 };
868 for entry in entries {
869 let entry = entry?;
870 let path = entry.path();
871 if path.extension().is_some_and(|ext| {
872 ext.eq_ignore_ascii_case("dll") || ext.eq_ignore_ascii_case("pyd")
873 }) {
874 if let Some(file_name) = path.file_name() {
875 fs_err::copy(&path, scripts.join(file_name))?;
876 }
877 }
878 }
879 }
880
881 match fs_err::read_dir(python_home) {
883 Ok(entries) => {
884 for entry in entries {
885 let entry = entry?;
886 let path = entry.path();
887 if path
888 .extension()
889 .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
890 {
891 if let Some(file_name) = path.file_name() {
892 fs_err::copy(&path, scripts.join(file_name))?;
893 }
894 }
895 }
896 }
897 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
898 Err(err) => {
899 return Err(err.into());
900 }
901 }
902
903 return Ok(());
904 }
905 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
906 Err(err) => {
907 return Err(err.into());
908 }
909 }
910
911 Err(Error::NotFound(base_python.user_display().to_string()))
912}