1pub mod apple_codesign;
19mod clobber_registry;
20mod driver;
21mod entry_point;
22pub mod link;
23pub mod link_script;
24mod python;
25mod transaction;
26pub mod unlink;
27
28mod installer;
29#[cfg(test)]
30mod test_utils;
31
32use std::{
33 cmp::Ordering,
34 collections::{binary_heap::PeekMut, BinaryHeap, HashMap, HashSet},
35 fs,
36 future::ready,
37 io::ErrorKind,
38 path::{Path, PathBuf},
39 sync::{Arc, Mutex},
40};
41
42pub use apple_codesign::AppleCodeSignBehavior;
43pub use driver::InstallDriver;
44use fs_err::tokio as tokio_fs;
45use futures::{stream::FuturesUnordered, FutureExt, StreamExt};
46pub use installer::{
47 result_record::InstallationResultRecord, Installer, InstallerError, LinkOptions, Reporter,
48};
49#[cfg(feature = "indicatif")]
50pub use installer::{
51 DefaultProgressFormatter, IndicatifReporter, IndicatifReporterBuilder, Placement,
52 ProgressFormatter,
53};
54use itertools::Itertools;
55pub use link::{link_file, LinkFileError, LinkMethod};
56pub use python::PythonInfo;
57use rattler_conda_types::{
58 package::{IndexJson, LinkJson, NoArchLinks, PackageFile, PathsEntry, PathsJson},
59 prefix::Prefix,
60 prefix_record, Platform,
61};
62use rayon::{
63 iter::Either,
64 prelude::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator},
65};
66use simple_spawn_blocking::Cancelled;
67use tokio::task::JoinError;
68use tracing::instrument;
69pub use transaction::{Transaction, TransactionError, TransactionOperation};
70pub use unlink::{empty_trash, unlink_package};
71
72pub use crate::install::entry_point::{get_windows_launcher, python_entry_point_template};
73use crate::install::{
74 clobber_registry::{ClobberRegistry, CLOBBERS_DIR_NAME},
75 entry_point::{create_unix_python_entry_point, create_windows_python_entry_point},
76};
77
78#[derive(Debug, thiserror::Error)]
80pub enum InstallError {
81 #[error("the operation was cancelled")]
83 Cancelled,
84
85 #[error("failed to read 'paths.json'")]
87 FailedToReadPathsJson(#[source] std::io::Error),
88
89 #[error("failed to read 'index.json'")]
91 FailedToReadIndexJson(#[source] std::io::Error),
92
93 #[error("failed to read 'link.json'")]
95 FailedToReadLinkJson(#[source] std::io::Error),
96
97 #[error("failed to link '{0}'")]
99 FailedToLink(PathBuf, #[source] LinkFileError),
100
101 #[error("failed to create directory '{0}'")]
103 FailedToCreateDirectory(PathBuf, #[source] std::io::Error),
104
105 #[error("target prefix is not UTF-8")]
107 TargetPrefixIsNotUtf8,
108
109 #[error("failed to create target directory")]
111 FailedToCreateTargetDirectory(#[source] std::io::Error),
112
113 #[error("cannot install noarch python package because there is no python version specified")]
116 MissingPythonInfo,
117
118 #[error("failed to create Python entry point")]
120 FailedToCreatePythonEntryPoint(#[source] std::io::Error),
121
122 #[error("failed to post process the environment (unclobbering)")]
125 PostProcessFailed(#[source] std::io::Error),
126}
127
128impl From<Cancelled> for InstallError {
129 fn from(_: Cancelled) -> Self {
130 InstallError::Cancelled
131 }
132}
133
134impl From<JoinError> for InstallError {
135 fn from(err: JoinError) -> Self {
136 if let Ok(panic) = err.try_into_panic() {
137 std::panic::resume_unwind(panic)
138 } else {
139 InstallError::Cancelled
140 }
141 }
142}
143
144#[derive(Default, Clone)]
148pub struct InstallOptions {
149 pub target_prefix: Option<PathBuf>,
157
158 pub paths_json: Option<PathsJson>,
164
165 pub index_json: Option<IndexJson>,
171
172 pub link_json: Option<Option<LinkJson>>,
186
187 pub allow_symbolic_links: Option<bool>,
195
196 pub allow_hard_links: Option<bool>,
206
207 pub allow_ref_links: Option<bool>,
218
219 pub platform: Option<Platform>,
223
224 pub python_info: Option<PythonInfo>,
236
237 pub apple_codesign_behavior: AppleCodeSignBehavior,
252}
253
254#[derive(Debug)]
255struct LinkPath {
256 entry: PathsEntry,
257 computed_path: PathBuf,
258 clobber_path: Option<PathBuf>,
259}
260
261#[instrument(skip_all, fields(package_dir = % package_dir.display()))]
268pub async fn link_package(
269 package_dir: &Path,
270 target_dir: &Prefix,
271 driver: &InstallDriver,
272 options: InstallOptions,
273) -> Result<Vec<prefix_record::PathsEntry>, InstallError> {
274 let target_prefix = options
276 .target_prefix
277 .as_deref()
278 .unwrap_or(target_dir)
279 .to_str()
280 .ok_or(InstallError::TargetPrefixIsNotUtf8)?
281 .to_owned();
282
283 let paths_json = read_paths_json(package_dir, driver, options.paths_json);
286 let index_json = read_index_json(package_dir, driver, options.index_json);
287 let (paths_json, index_json) = tokio::try_join!(paths_json, index_json)?;
288
289 if index_json.noarch.is_python() && options.python_info.is_none() {
292 return Err(InstallError::MissingPythonInfo);
293 }
294
295 let link_json = if index_json.noarch.is_python() {
297 read_link_json(package_dir, driver, options.link_json.flatten()).await?
298 } else {
299 None
300 };
301
302 let (allow_symbolic_links, allow_hard_links) = tokio::join!(
304 match options.allow_symbolic_links {
306 Some(value) => ready(value).left_future(),
307 None => can_create_symlinks(target_dir).right_future(),
308 },
309 match options.allow_hard_links {
311 Some(value) => ready(value).left_future(),
312 None => can_create_hardlinks(target_dir, package_dir).right_future(),
313 },
314 );
315 let allow_ref_links = options.allow_ref_links.unwrap_or_else(|| {
316 match reflink_copy::check_reflink_support(package_dir, target_dir) {
317 Ok(reflink_copy::ReflinkSupport::Supported) => true,
318 Ok(reflink_copy::ReflinkSupport::NotSupported) | Err(_) => false,
319 Ok(reflink_copy::ReflinkSupport::Unknown) => allow_hard_links,
320 }
321 });
322
323 let platform = options.platform.unwrap_or(Platform::current());
325
326 let final_paths = compute_paths(&index_json, &paths_json, options.python_info.as_ref());
328
329 let clobber_paths = Arc::new(
331 driver
332 .clobber_registry()
333 .register_paths(&index_json, &final_paths),
334 );
335
336 let final_paths: Vec<LinkPath> = final_paths
337 .into_iter()
338 .map(|el| {
339 let (entry, computed_path) = el;
340 let clobber_path = clobber_paths.get(&computed_path).cloned();
341 LinkPath {
342 entry,
343 computed_path,
344 clobber_path,
345 }
346 })
347 .collect();
348
349 let mut directories_to_construct = HashSet::new();
351 for link_path in &final_paths {
352 let mut current_path = link_path.computed_path.parent();
353 while let Some(path) = current_path {
354 if !path.as_os_str().is_empty() && directories_to_construct.insert(path.to_path_buf()) {
355 current_path = path.parent();
356 } else {
357 break;
358 }
359 }
360
361 let clobber_path = link_path.clobber_path.as_ref();
365 let mut current_clobber_path = clobber_path.and_then(|p| p.parent());
366 while let Some(path) = current_clobber_path {
367 if !path.as_os_str().is_empty() && directories_to_construct.insert(path.to_path_buf()) {
368 current_clobber_path = path.parent();
369 } else {
370 break;
371 }
372 }
373 }
374
375 let directories_target_dir = target_dir.path().to_path_buf();
376 driver
377 .run_blocking_io_task(move || {
378 for directory in directories_to_construct.into_iter().sorted() {
379 let full_path = directories_target_dir.join(directory);
380 match fs::create_dir(&full_path) {
381 Ok(_) => (),
382 Err(e) if e.kind() == ErrorKind::AlreadyExists => (),
383 Err(e) => return Err(InstallError::FailedToCreateDirectory(full_path, e)),
384 }
385 }
386 Ok(())
387 })
388 .await?;
389
390 let python_info = options.python_info.map(Arc::new);
393
394 let mut pending_futures = FuturesUnordered::new();
396 let mut number_of_paths_entries = 0;
397 for link_path in final_paths {
398 let entry = link_path.entry;
399 let package_dir = package_dir.to_owned();
400 let target_dir = target_dir.to_owned();
401 let target_prefix = target_prefix.clone();
402
403 let install_future = async move {
404 let _permit = driver.acquire_io_permit().await;
405
406 let cloned_entry = entry.clone();
410 let is_clobber = link_path.clobber_path.is_some();
411 let result = match tokio::task::spawn_blocking(move || {
412 link_file(
413 &cloned_entry,
414 link_path.clobber_path.unwrap_or(link_path.computed_path),
415 &package_dir,
416 &target_dir,
417 &target_prefix,
418 allow_symbolic_links && !cloned_entry.no_link,
419 allow_hard_links && !cloned_entry.no_link,
420 allow_ref_links && !cloned_entry.no_link,
421 platform,
422 options.apple_codesign_behavior,
423 )
424 })
425 .await
426 .map_err(JoinError::try_into_panic)
427 {
428 Ok(Ok(linked_file)) => linked_file,
429 Ok(Err(e)) => {
430 return Err(InstallError::FailedToLink(entry.relative_path.clone(), e));
431 }
432 Err(Ok(payload)) => std::panic::resume_unwind(payload),
433 Err(Err(_err)) => return Err(InstallError::Cancelled),
434 };
435
436 let paths_entry = prefix_record::PathsEntry {
438 relative_path: result.relative_path,
439 original_path: if is_clobber {
440 Some(entry.relative_path)
441 } else {
442 None
443 },
444 path_type: entry.path_type.into(),
445 no_link: entry.no_link,
446 sha256: entry.sha256,
447 sha256_in_prefix: if Some(result.sha256) == entry.sha256 {
449 None
450 } else {
451 Some(result.sha256)
452 },
453 size_in_bytes: Some(result.file_size),
454 file_mode: match result.method {
455 LinkMethod::Patched(file_mode) => Some(file_mode),
456 _ => None,
457 },
458 prefix_placeholder: entry
459 .prefix_placeholder
460 .as_ref()
461 .map(|p| p.placeholder.clone()),
462 };
463
464 Ok(vec![(number_of_paths_entries, paths_entry)])
465 };
466
467 pending_futures.push(install_future.boxed());
468 number_of_paths_entries += 1;
469 }
470
471 if let Some(link_json) = link_json {
477 let entry_points = match link_json.noarch {
479 NoArchLinks::Python(entry_points) => entry_points.entry_points,
480 NoArchLinks::Generic => {
481 unreachable!("we only use link.json for noarch: python packages")
482 }
483 };
484
485 let python_info = python_info
487 .clone()
488 .expect("should be safe because its checked above that this contains a value");
489
490 for entry_point in entry_points {
494 let python_info = python_info.clone();
495 let target_dir = target_dir.to_owned();
496 let target_prefix = target_prefix.clone();
497
498 let entry_point_fut = async move {
499 let _permit = driver.acquire_io_permit().await;
501
502 let entries = if platform.is_windows() {
503 match create_windows_python_entry_point(
504 &target_dir,
505 &target_prefix,
506 &entry_point,
507 &python_info,
508 &platform,
509 ) {
510 Ok([a, b]) => vec![
511 (number_of_paths_entries, a),
512 (number_of_paths_entries + 1, b),
513 ],
514 Err(e) => return Err(InstallError::FailedToCreatePythonEntryPoint(e)),
515 }
516 } else {
517 match create_unix_python_entry_point(
518 &target_dir,
519 &target_prefix,
520 &entry_point,
521 &python_info,
522 ) {
523 Ok(a) => vec![(number_of_paths_entries, a)],
524 Err(e) => return Err(InstallError::FailedToCreatePythonEntryPoint(e)),
525 }
526 };
527
528 Ok(entries)
529 };
530
531 pending_futures.push(entry_point_fut.boxed());
532 number_of_paths_entries += if platform.is_windows() { 2 } else { 1 };
533 }
534 }
535
536 let mut paths = Vec::with_capacity(number_of_paths_entries);
547 let mut out_of_order_queue =
548 BinaryHeap::<OrderWrapper<prefix_record::PathsEntry>>::with_capacity(100);
549 while let Some(link_result) = pending_futures.next().await {
550 for (index, data) in link_result? {
551 if index == paths.len() {
552 paths.push(data);
555
556 while let Some(next_output) = out_of_order_queue.peek_mut() {
560 if next_output.index == paths.len() {
561 paths.push(PeekMut::pop(next_output).data);
562 } else {
563 break;
564 }
565 }
566 } else {
567 out_of_order_queue.push(OrderWrapper { index, data });
571 }
572 }
573 }
574 debug_assert_eq!(
575 paths.len(),
576 paths.capacity(),
577 "some futures where not added to the result"
578 );
579
580 Ok(paths)
581}
582
583#[instrument(skip_all, fields(package_dir = % package_dir.display()))]
590pub fn link_package_sync(
591 package_dir: &Path,
592 target_dir: &Prefix,
593 clobber_registry: Arc<Mutex<ClobberRegistry>>,
594 options: InstallOptions,
595) -> Result<Vec<prefix_record::PathsEntry>, InstallError> {
596 let target_prefix = options
598 .target_prefix
599 .as_deref()
600 .unwrap_or(target_dir)
601 .to_str()
602 .ok_or(InstallError::TargetPrefixIsNotUtf8)?
603 .to_owned();
604
605 let paths_json = options.paths_json.map_or_else(
608 || {
609 PathsJson::from_package_directory_with_deprecated_fallback(package_dir)
610 .map_err(InstallError::FailedToReadPathsJson)
611 },
612 Ok,
613 )?;
614 let index_json = options.index_json.map_or_else(
615 || {
616 IndexJson::from_package_directory(package_dir)
617 .map_err(InstallError::FailedToReadIndexJson)
618 },
619 Ok,
620 )?;
621
622 if index_json.noarch.is_python() && options.python_info.is_none() {
625 return Err(InstallError::MissingPythonInfo);
626 }
627
628 let link_json = if index_json.noarch.is_python() {
630 options.link_json.flatten().map_or_else(
631 || {
632 LinkJson::from_package_directory(package_dir)
633 .map_or_else(
634 |e| {
635 if e.kind() == ErrorKind::NotFound {
637 Ok(None)
638 } else {
639 Err(e)
640 }
641 },
642 |link_json| Ok(Some(link_json)),
643 )
644 .map_err(InstallError::FailedToReadLinkJson)
645 },
646 |value| Ok(Some(value)),
647 )?
648 } else {
649 None
650 };
651
652 let allow_symbolic_links = options
654 .allow_symbolic_links
655 .unwrap_or_else(|| can_create_symlinks_sync(target_dir));
656 let allow_hard_links = options
657 .allow_hard_links
658 .unwrap_or_else(|| can_create_hardlinks_sync(target_dir, package_dir));
659 let allow_ref_links = options.allow_ref_links.unwrap_or_else(|| {
660 match reflink_copy::check_reflink_support(package_dir, target_dir) {
661 Ok(reflink_copy::ReflinkSupport::Supported) => true,
662 Ok(reflink_copy::ReflinkSupport::NotSupported) | Err(_) => false,
663 Ok(reflink_copy::ReflinkSupport::Unknown) => allow_hard_links,
664 }
665 });
666
667 let platform = options.platform.unwrap_or(Platform::current());
669
670 let final_paths = compute_paths(&index_json, &paths_json, options.python_info.as_ref());
672
673 let clobber_paths = clobber_registry
675 .lock()
676 .unwrap()
677 .register_paths(&index_json, &final_paths);
678
679 let final_paths = final_paths.into_iter().map(|el| {
680 let (entry, computed_path) = el;
681 let clobber_path = clobber_paths.get(&computed_path).cloned();
682 LinkPath {
683 entry,
684 computed_path,
685 clobber_path,
686 }
687 });
688
689 let mut directories_to_construct = HashSet::new();
691 let mut paths_by_directory = HashMap::new();
692 for link_path in final_paths {
693 let Some(entry_parent) = link_path.computed_path.parent() else {
694 continue;
695 };
696
697 let mut current_path = Some(entry_parent);
699 while let Some(path) = current_path {
700 if !path.as_os_str().is_empty() && directories_to_construct.insert(path.to_path_buf()) {
701 current_path = path.parent();
702 } else {
703 break;
704 }
705 }
706
707 let clobber_path = link_path.clobber_path.as_ref();
711 let mut current_path = clobber_path.and_then(|p| p.parent());
712 while let Some(path) = current_path {
713 if !path.as_os_str().is_empty() && directories_to_construct.insert(path.to_path_buf()) {
714 current_path = path.parent();
715 } else {
716 break;
717 }
718 }
719
720 paths_by_directory
722 .entry(entry_parent.to_path_buf())
723 .or_insert_with(Vec::new)
724 .push(link_path);
725 }
726
727 let mut created_directories = HashSet::new();
728 let mut reflinked_files = HashMap::new();
729 for directory in directories_to_construct
730 .into_iter()
731 .sorted_by(|a, b| a.components().count().cmp(&b.components().count()))
732 {
733 let full_path = target_dir.path().join(&directory);
734
735 if created_directories
737 .iter()
738 .any(|dir| directory.starts_with(dir))
739 {
740 continue;
741 }
742
743 if full_path.exists() {
745 continue;
746 }
747
748 if allow_ref_links
749 && cfg!(target_os = "macos")
750 && !directory.starts_with(CLOBBERS_DIR_NAME)
751 && !index_json.noarch.is_python()
752 {
753 match reflink_copy::reflink(package_dir.join(&directory), &full_path) {
756 Ok(_) => {
757 created_directories.insert(directory.clone());
758 let (matching, non_matching): (HashMap<_, _>, HashMap<_, _>) =
760 paths_by_directory
761 .drain()
762 .partition(|(k, _)| k.starts_with(&directory));
763
764 reflinked_files.extend(matching);
766 paths_by_directory = non_matching;
768 }
769 Err(e) if e.kind() == ErrorKind::AlreadyExists => (),
770 Err(e) => return Err(InstallError::FailedToCreateDirectory(full_path, e)),
771 }
772 } else {
773 match fs::create_dir(&full_path) {
774 Ok(_) => (),
775 Err(e) if e.kind() == ErrorKind::AlreadyExists => (),
776 Err(e) => return Err(InstallError::FailedToCreateDirectory(full_path, e)),
777 }
778 }
779 }
780
781 let mut reflinked_paths_entries = Vec::new();
786 for (parent_dir, files) in reflinked_files {
787 for link_path in files {
791 if link_path.clobber_path.is_some() || link_path.entry.prefix_placeholder.is_some() {
792 paths_by_directory
793 .entry(parent_dir.clone())
794 .or_insert_with(Vec::new)
795 .push(link_path);
796 } else {
797 let entry = link_path.entry;
798 reflinked_paths_entries.push(prefix_record::PathsEntry {
799 relative_path: entry.relative_path,
800 path_type: entry.path_type.into(),
801 no_link: entry.no_link,
802 sha256: entry.sha256,
803 size_in_bytes: entry.size_in_bytes,
804 original_path: None,
806 sha256_in_prefix: None,
807 file_mode: None,
808 prefix_placeholder: None,
809 });
810 }
811 }
812 }
813
814 let python_info = options.python_info;
817
818 let link_target_prefix = target_prefix.clone();
820 let package_dir = package_dir.to_path_buf();
821 let mut paths = paths_by_directory
822 .into_values()
823 .collect_vec()
824 .into_par_iter()
825 .with_min_len(100)
826 .flat_map(move |entries_in_subdir| {
827 let mut path_entries = Vec::with_capacity(entries_in_subdir.len());
828 for link_path in entries_in_subdir {
829 let entry = link_path.entry;
830
831 let is_clobber = link_path.clobber_path.is_some();
832 let link_result = link_file(
833 &entry,
834 link_path
835 .clobber_path
836 .unwrap_or(link_path.computed_path.clone()),
837 &package_dir,
838 target_dir,
839 &link_target_prefix,
840 allow_symbolic_links && !entry.no_link,
841 allow_hard_links && !entry.no_link,
842 allow_ref_links && !entry.no_link,
843 platform,
844 options.apple_codesign_behavior,
845 );
846
847 let result = match link_result {
848 Ok(linked_file) => linked_file,
849 Err(e) => {
850 return vec![Err(InstallError::FailedToLink(
851 entry.relative_path.clone(),
852 e,
853 ))];
854 }
855 };
856
857 path_entries.push(Ok(prefix_record::PathsEntry {
859 relative_path: result.relative_path,
860 original_path: if is_clobber {
861 Some(link_path.computed_path)
862 } else {
863 None
864 },
865 path_type: entry.path_type.into(),
866 no_link: entry.no_link,
867 sha256: entry.sha256,
868 sha256_in_prefix: if Some(result.sha256) == entry.sha256 {
870 None
871 } else {
872 Some(result.sha256)
873 },
874 size_in_bytes: Some(result.file_size),
875 file_mode: match result.method {
876 LinkMethod::Patched(file_mode) => Some(file_mode),
877 _ => None,
878 },
879 prefix_placeholder: entry
880 .prefix_placeholder
881 .as_ref()
882 .map(|p| p.placeholder.clone()),
883 }));
884 }
885
886 path_entries
887 })
888 .collect::<Result<Vec<_>, _>>()?;
889
890 paths.extend(reflinked_paths_entries);
891
892 if let Some(link_json) = link_json {
898 let entry_points = match link_json.noarch {
900 NoArchLinks::Python(entry_points) => entry_points.entry_points,
901 NoArchLinks::Generic => {
902 unreachable!("we only use link.json for noarch: python packages")
903 }
904 };
905
906 let python_info = python_info
908 .clone()
909 .expect("should be safe because its checked above that this contains a value");
910
911 let target_prefix = target_prefix.clone();
912
913 let mut entry_point_paths = if platform.is_windows() {
917 entry_points
918 .into_iter()
919 .flat_map(move |entry_point| {
922 match create_windows_python_entry_point(
923 target_dir,
924 &target_prefix,
925 &entry_point,
926 &python_info,
927 &platform,
928 ) {
929 Ok([a, b]) => Either::Left([Ok(a), Ok(b)].into_iter()),
930 Err(e) => Either::Right(std::iter::once(Err(
931 InstallError::FailedToCreatePythonEntryPoint(e),
932 ))),
933 }
934 })
935 .collect::<Result<Vec<_>, _>>()?
936 } else {
937 entry_points
938 .into_iter()
939 .map(move |entry_point| {
942 match create_unix_python_entry_point(
943 target_dir,
944 &target_prefix,
945 &entry_point,
946 &python_info,
947 ) {
948 Ok(a) => Ok(a),
949 Err(e) => Err(InstallError::FailedToCreatePythonEntryPoint(e)),
950 }
951 })
952 .collect::<Result<_, _>>()?
953 };
954
955 paths.append(&mut entry_point_paths);
956 };
957
958 Ok(paths)
959}
960
961fn compute_paths(
962 index_json: &IndexJson,
963 paths_json: &PathsJson,
964 python_info: Option<&PythonInfo>,
965) -> Vec<(rattler_conda_types::package::PathsEntry, PathBuf)> {
966 let mut final_paths = Vec::with_capacity(paths_json.paths.len());
967 for entry in &paths_json.paths {
968 let path = if index_json.noarch.is_python() {
969 python_info
970 .unwrap()
971 .get_python_noarch_target_path(&entry.relative_path)
972 .to_path_buf()
973 } else {
974 entry.relative_path.clone()
975 };
976
977 final_paths.push((entry.clone(), path));
978 }
979 final_paths
980}
981
982async fn read_paths_json(
985 package_dir: &Path,
986 driver: &InstallDriver,
987 paths_json: Option<PathsJson>,
988) -> Result<PathsJson, InstallError> {
989 if let Some(paths_json) = paths_json {
990 Ok(paths_json)
991 } else {
992 let package_dir = package_dir.to_owned();
993 driver
994 .run_blocking_io_task(move || {
995 PathsJson::from_package_directory_with_deprecated_fallback(&package_dir)
996 .map_err(InstallError::FailedToReadPathsJson)
997 })
998 .await
999 }
1000}
1001
1002async fn read_index_json(
1005 package_dir: &Path,
1006 driver: &InstallDriver,
1007 index_json: Option<IndexJson>,
1008) -> Result<IndexJson, InstallError> {
1009 if let Some(index) = index_json {
1010 Ok(index)
1011 } else {
1012 let package_dir = package_dir.to_owned();
1013 driver
1014 .run_blocking_io_task(move || {
1015 IndexJson::from_package_directory(package_dir)
1016 .map_err(InstallError::FailedToReadIndexJson)
1017 })
1018 .await
1019 }
1020}
1021
1022async fn read_link_json(
1025 package_dir: &Path,
1026 driver: &InstallDriver,
1027 index_json: Option<LinkJson>,
1028) -> Result<Option<LinkJson>, InstallError> {
1029 if let Some(index) = index_json {
1030 Ok(Some(index))
1031 } else {
1032 let package_dir = package_dir.to_owned();
1033 driver
1034 .run_blocking_io_task(move || {
1035 LinkJson::from_package_directory(package_dir)
1036 .map_or_else(
1037 |e| {
1038 if e.kind() == ErrorKind::NotFound {
1040 Ok(None)
1041 } else {
1042 Err(e)
1043 }
1044 },
1045 |link_json| Ok(Some(link_json)),
1046 )
1047 .map_err(InstallError::FailedToReadLinkJson)
1048 })
1049 .await
1050 }
1051}
1052
1053fn can_create_symlinks_sync(target_dir: &Prefix) -> bool {
1055 let uuid = uuid::Uuid::new_v4();
1056 let symlink_path = target_dir.path().join(format!("symtest_{uuid}"));
1057 #[cfg(windows)]
1058 let result = std::os::windows::fs::symlink_file("./", &symlink_path);
1059 #[cfg(unix)]
1060 let result = fs_err::os::unix::fs::symlink("./", &symlink_path);
1061 match result {
1062 Ok(_) => {
1063 if let Err(e) = fs_err::remove_file(&symlink_path) {
1064 tracing::warn!(
1065 "failed to delete temporary file '{}': {e}",
1066 symlink_path.display()
1067 );
1068 }
1069 true
1070 }
1071 Err(e) => {
1072 tracing::debug!(
1073 "failed to create symlink in target directory: {e}. Disabling use of symlinks."
1074 );
1075 false
1076 }
1077 }
1078}
1079
1080struct OrderWrapper<T> {
1083 index: usize,
1084 data: T,
1085}
1086
1087impl<T> PartialEq for OrderWrapper<T> {
1088 fn eq(&self, other: &Self) -> bool {
1089 self.index == other.index
1090 }
1091}
1092
1093impl<T> Eq for OrderWrapper<T> {}
1094
1095impl<T> PartialOrd for OrderWrapper<T> {
1096 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1097 Some(self.cmp(other))
1098 }
1099}
1100
1101impl<T> Ord for OrderWrapper<T> {
1102 fn cmp(&self, other: &Self) -> Ordering {
1103 other.index.cmp(&self.index)
1105 }
1106}
1107
1108async fn can_create_symlinks(target_dir: &Prefix) -> bool {
1110 let uuid = uuid::Uuid::new_v4();
1111 let symlink_path = target_dir.path().join(format!("symtest_{uuid}"));
1112 #[cfg(windows)]
1113 let result = tokio_fs::symlink_file("./", &symlink_path).await;
1114 #[cfg(unix)]
1115 let result = tokio_fs::symlink("./", &symlink_path).await;
1116 match result {
1117 Ok(_) => {
1118 if let Err(e) = tokio_fs::remove_file(&symlink_path).await {
1119 tracing::warn!(
1120 "failed to delete temporary file '{}': {e}",
1121 symlink_path.display()
1122 );
1123 }
1124 true
1125 }
1126 Err(e) => {
1127 tracing::debug!(
1128 "failed to create symlink in target directory: {e}. Disabling use of symlinks."
1129 );
1130 false
1131 }
1132 }
1133}
1134
1135async fn can_create_hardlinks(target_dir: &Prefix, package_dir: &Path) -> bool {
1138 paths_have_same_filesystem(target_dir, package_dir).await
1139}
1140
1141fn can_create_hardlinks_sync(target_dir: &Prefix, package_dir: &Path) -> bool {
1144 paths_have_same_filesystem_sync(target_dir.path(), package_dir)
1145}
1146
1147#[cfg(unix)]
1149async fn paths_have_same_filesystem(a: &Prefix, b: &Path) -> bool {
1150 use std::os::unix::fs::MetadataExt;
1151 match tokio::join!(tokio_fs::metadata(a.path()), tokio_fs::metadata(b)) {
1152 (Ok(a), Ok(b)) => a.dev() == b.dev(),
1153 _ => false,
1154 }
1155}
1156
1157#[cfg(unix)]
1159fn paths_have_same_filesystem_sync(a: &Path, b: &Path) -> bool {
1160 use std::os::unix::fs::MetadataExt;
1161 let a = std::fs::metadata(a);
1162 let b = std::fs::metadata(b);
1163 match (a, b) {
1164 (Ok(a), Ok(b)) => a.dev() == b.dev(),
1165 _ => false,
1166 }
1167}
1168
1169#[cfg(not(unix))]
1171async fn paths_have_same_filesystem(a: &Path, b: &Path) -> bool {
1172 match (a.canonicalize(), b.canonicalize()) {
1173 (Ok(a), Ok(b)) => a.components().next() == b.components().next(),
1174 _ => false,
1175 }
1176}
1177
1178#[cfg(not(unix))]
1180fn paths_have_same_filesystem_sync(a: &Path, b: &Path) -> bool {
1181 match (a.canonicalize(), b.canonicalize()) {
1182 (Ok(a), Ok(b)) => a.components().next() == b.components().next(),
1183 _ => false,
1184 }
1185}
1186
1187#[cfg(test)]
1188mod test {
1189 use std::{env::temp_dir, process::Command, str::FromStr};
1190
1191 use crate::{
1192 get_test_data_dir,
1193 install::{link_package, InstallDriver, InstallOptions, Prefix, PythonInfo},
1194 package_cache::PackageCache,
1195 };
1196 use futures::{stream, StreamExt};
1197 use rattler_conda_types::{
1198 package::CondaArchiveIdentifier, ExplicitEnvironmentSpec, Platform, Version,
1199 };
1200 use rattler_lock::LockFile;
1201 use rattler_networking::LazyClient;
1202 use tempfile::tempdir;
1203 use url::Url;
1204
1205 #[tracing_test::traced_test]
1206 #[tokio::test]
1207 pub async fn test_explicit_lock() {
1208 let current_platform = Platform::current();
1210 let explicit_env_path =
1211 get_test_data_dir().join(format!("python/explicit-env-{current_platform}.txt"));
1212 let env = ExplicitEnvironmentSpec::from_path(&explicit_env_path).unwrap();
1213
1214 assert_eq!(
1215 env.platform,
1216 Some(current_platform),
1217 "the platform for which the explicit lock file was created does not match the current platform"
1218 );
1219
1220 test_install_python(
1221 env.packages.into_iter().map(|p| p.url),
1222 "explicit",
1223 current_platform,
1224 )
1225 .await;
1226 }
1227
1228 #[tracing_test::traced_test]
1229 #[tokio::test]
1230 pub async fn test_conda_lock() {
1231 let lock_path = get_test_data_dir().join("conda-lock/v4/python-lock.yml");
1233 let lock = LockFile::from_path(&lock_path).unwrap();
1234
1235 let current_platform = Platform::current();
1236 let lock_env = lock
1237 .default_environment()
1238 .expect("no default environment in lock file");
1239
1240 let Some(packages) = lock_env.packages(current_platform) else {
1241 panic!(
1242 "the platform for which the explicit lock file was created does not match the current platform"
1243 )
1244 };
1245
1246 test_install_python(
1247 packages.filter_map(|p| p.as_conda()?.location().as_url().cloned()),
1248 "conda-lock",
1249 current_platform,
1250 )
1251 .await;
1252 }
1253
1254 pub async fn test_install_python(
1255 urls: impl Iterator<Item = Url>,
1256 cache_name: &str,
1257 platform: Platform,
1258 ) {
1259 let package_cache = PackageCache::new(temp_dir().join("rattler").join(cache_name));
1263
1264 let client = LazyClient::default();
1266
1267 let python_version =
1269 PythonInfo::from_version(&Version::from_str("3.11.0").unwrap(), None, platform)
1270 .unwrap();
1271
1272 let install_driver = InstallDriver::default();
1274 let target_dir = tempdir().unwrap();
1275 let prefix_path = Prefix::create(target_dir.path()).unwrap();
1276 stream::iter(urls)
1277 .for_each_concurrent(Some(50), |package_url| {
1278 let client = client.clone();
1279 let package_cache = &package_cache;
1280 let install_driver = &install_driver;
1281 let python_version = &python_version;
1282 let prefix_path = prefix_path.clone();
1283 async move {
1284 let package_info = CondaArchiveIdentifier::try_from_url(&package_url).unwrap();
1286 let package_cache_lock = package_cache
1287 .get_or_fetch_from_url(
1288 package_info,
1289 package_url.clone(),
1290 client.clone(),
1291 None,
1292 )
1293 .await
1294 .unwrap();
1295
1296 link_package(
1298 package_cache_lock.path(),
1299 &prefix_path,
1300 install_driver,
1301 InstallOptions {
1302 python_info: Some(python_version.clone()),
1303 ..InstallOptions::default()
1304 },
1305 )
1306 .await
1307 .unwrap();
1308 }
1309 })
1310 .await;
1311
1312 let python_path = if Platform::current().is_windows() {
1314 "python.exe"
1315 } else {
1316 "bin/python"
1317 };
1318 let python_version = Command::new(target_dir.path().join(python_path))
1319 .arg("--version")
1320 .output()
1321 .unwrap();
1322
1323 assert!(python_version.status.success());
1324 assert_eq!(
1325 String::from_utf8_lossy(&python_version.stdout).trim(),
1326 "Python 3.11.0"
1327 );
1328 }
1329
1330 #[tracing_test::traced_test]
1331 #[tokio::test]
1332 async fn test_prefix_paths() {
1333 let environment_dir = tempfile::TempDir::new().unwrap();
1334 let package_dir = tempfile::TempDir::new().unwrap();
1335
1336 let package_path = tools::download_and_cache_file_async(
1337 "https://conda.anaconda.org/conda-forge/win-64/ruff-0.0.171-py310h298983d_0.conda"
1338 .parse()
1339 .unwrap(),
1340 "25c755b97189ee066576b4ae3999d5e7ff4406d236b984742194e63941838dcd",
1341 )
1342 .await
1343 .unwrap();
1344
1345 rattler_package_streaming::fs::extract(&package_path, package_dir.path()).unwrap();
1347
1348 let install_driver = InstallDriver::default();
1349
1350 let paths = link_package(
1352 package_dir.path(),
1353 &Prefix::create(environment_dir.path()).unwrap(),
1354 &install_driver,
1355 InstallOptions::default(),
1356 )
1357 .await
1358 .unwrap();
1359
1360 insta::assert_yaml_snapshot!(paths);
1361 }
1362}