rattler/install/
mod.rs

1//! This module contains the logic to install a package into a prefix. The main
2//! entry point is the [`link_package`] function.
3//!
4//! The [`link_package`] function takes a package directory and a target
5//! directory. The package directory is the directory that contains the
6//! extracted package archive. The target directory is the directory into which
7//! the package should be installed. The target directory is also called
8//! the "prefix".
9//!
10//! The [`link_package`] function will read the `paths.json` file from the
11//! package directory and link all files specified in that file into the target
12//! directory. The `paths.json` file contains a list of files that should be
13//! installed and how they should be installed. For example, the `paths.json`
14//! file might contain a file that should be copied into the target directory.
15//! Or it might contain a file that should be linked into the target directory.
16//! The `paths.json` file also contains a SHA256 hash for each file. This hash
17//! is used to verify that the file was not tampered with.
18pub 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/// An error that might occur when installing a package.
79#[derive(Debug, thiserror::Error)]
80pub enum InstallError {
81    /// The operation was cancelled.
82    #[error("the operation was cancelled")]
83    Cancelled,
84
85    /// The paths.json file could not be read.
86    #[error("failed to read 'paths.json'")]
87    FailedToReadPathsJson(#[source] std::io::Error),
88
89    /// The index.json file could not be read.
90    #[error("failed to read 'index.json'")]
91    FailedToReadIndexJson(#[source] std::io::Error),
92
93    /// The link.json file could not be read.
94    #[error("failed to read 'link.json'")]
95    FailedToReadLinkJson(#[source] std::io::Error),
96
97    /// A file could not be linked.
98    #[error("failed to link '{0}'")]
99    FailedToLink(PathBuf, #[source] LinkFileError),
100
101    /// A directory could not be created.
102    #[error("failed to create directory '{0}'")]
103    FailedToCreateDirectory(PathBuf, #[source] std::io::Error),
104
105    /// The target prefix is not UTF-8.
106    #[error("target prefix is not UTF-8")]
107    TargetPrefixIsNotUtf8,
108
109    /// Failed to create the target directory.
110    #[error("failed to create target directory")]
111    FailedToCreateTargetDirectory(#[source] std::io::Error),
112
113    /// A noarch package could not be installed because no python version was
114    /// specified.
115    #[error("cannot install noarch python package because there is no python version specified")]
116    MissingPythonInfo,
117
118    /// Failed to create a python entry point for a noarch package.
119    #[error("failed to create Python entry point")]
120    FailedToCreatePythonEntryPoint(#[source] std::io::Error),
121
122    /// When post-processing of the environment fails.
123    /// Post-processing involves removing clobbered paths.
124    #[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/// Additional options to pass to [`link_package`] to modify the installation
145/// process. Using [`InstallOptions::default`] works in most cases unless you
146/// want specific control over the installation process.
147#[derive(Default, Clone)]
148pub struct InstallOptions {
149    /// When files are copied/linked to the target directory hardcoded paths in
150    /// these files are "patched". The hardcoded paths are replaced with the
151    /// full path of the target directory, also called the "prefix".
152    ///
153    /// However, in exceptional cases you might want to use a different prefix
154    /// than the one that is being installed to. This field allows you to do
155    /// that. When its set this is used instead of the target directory.
156    pub target_prefix: Option<PathBuf>,
157
158    /// Instead of reading the `paths.json` file from the package directory
159    /// itself, use the data specified here.
160    ///
161    /// This is sometimes useful to avoid reading the file twice or when you
162    /// want to modify installation process externally.
163    pub paths_json: Option<PathsJson>,
164
165    /// Instead of reading the `index.json` file from the package directory
166    /// itself, use the data specified here.
167    ///
168    /// This is sometimes useful to avoid reading the file twice or when you
169    /// want to modify installation process externally.
170    pub index_json: Option<IndexJson>,
171
172    /// Instead of reading the `link.json` file from the package directory
173    /// itself, use the data specified here.
174    ///
175    /// This is sometimes useful to avoid reading the file twice or when you
176    /// want to modify installation process externally.
177    ///
178    /// Because the the `link.json` file is optional this fields is using a
179    /// doubly wrapped Option. The first `Option` is to indicate whether or
180    /// not this value is set. The second Option is the [`LinkJson`] to use
181    /// or `None` if you want to force that there is no [`LinkJson`].
182    ///
183    /// This struct is only used if the package to be linked is a noarch Python
184    /// package.
185    pub link_json: Option<Option<LinkJson>>,
186
187    /// Whether or not to use symbolic links where possible. If this is set to
188    /// `Some(false)` symlinks are disabled, if set to `Some(true)` symbolic
189    /// links are always used when specified in the [`info/paths.json`] file
190    /// even if this is not supported. If the value is set to `None`
191    /// symbolic links are only used if they are supported.
192    ///
193    /// Windows only supports symbolic links in specific cases.
194    pub allow_symbolic_links: Option<bool>,
195
196    /// Whether or not to use hard links where possible. If this is set to
197    /// `Some(false)` the use of hard links is disabled, if set to
198    /// `Some(true)` hard links are always used when specified
199    /// in the [`info/paths.json`] file even if this is not supported. If the
200    /// value is set to `None` hard links are only used if they are
201    /// supported. A dummy hardlink is created to determine support.
202    ///
203    /// Hard links are supported by most OSes but often require that the hard
204    /// link and its content are on the same filesystem.
205    pub allow_hard_links: Option<bool>,
206
207    /// Whether or not to use ref links where possible. If this is set to
208    /// `Some(false)` the use of hard links is disabled, if set to
209    /// `Some(true)` ref links are always used when hard links are specified
210    /// in the [`info/paths.json`] file even if this is not supported. If the
211    /// value is set to `None` ref links are only used if they are
212    /// supported.
213    ///
214    /// Ref links are only support by a small number of OSes and filesystems. If
215    /// reflinking fails for whatever reason the files are hardlinked
216    /// instead (if allowed).
217    pub allow_ref_links: Option<bool>,
218
219    /// The platform for which the package is installed. Some operations like
220    /// signing require different behavior depending on the platform. If the
221    /// field is set to `None` the current platform is used.
222    pub platform: Option<Platform>,
223
224    /// Python version information of the python distribution installed within
225    /// the environment. This is only used when installing noarch Python
226    /// packages. Noarch python packages are python packages that contain
227    /// python source code that has to be installed in the correct
228    /// site-packages directory based on the version of python. This
229    /// site-packages directory depends on the version of python, therefor
230    /// it must be provided when linking.
231    ///
232    /// If you're installing a noarch python package and do not provide this
233    /// field, the [`link_package`] function will return
234    /// [`InstallError::MissingPythonInfo`].
235    pub python_info: Option<PythonInfo>,
236
237    /// For binaries on macOS (both Intel and Apple Silicon), binaries need to be signed
238    /// with an ad-hoc certificate to properly work when their signature has been invalidated
239    /// by prefix replacement (modifying binary content). This field controls whether or not to do that.
240    /// Code signing is executed when the target platform is macOS. By default, codesigning
241    /// will fail the installation if it fails. This behavior can be changed by setting
242    /// this field to `AppleCodeSignBehavior::Ignore` or
243    /// `AppleCodeSignBehavior::DoNothing`.
244    ///
245    /// To sign the binaries, the `/usr/bin/codesign` executable is called with
246    /// `--force`, `--sign -`, and `--preserve-metadata=entitlements` arguments.
247    /// The `--force` argument is used to overwrite existing signatures, the `--sign -`
248    /// argument is used to sign with an ad-hoc certificate (which does not use an identity
249    /// at all), and `--preserve-metadata=entitlements` preserves the original entitlements
250    /// from the binary (required for programs that need specific permissions like virtualization).
251    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/// Given an extracted package archive (`package_dir`), installs its files to
262/// the `target_dir`.
263///
264/// Returns a [`PathsEntry`] for every file that was linked into the target
265/// directory. The entries are ordered in the same order as they appear in the
266/// `paths.json` file of the package.
267#[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    // Determine the target prefix for linking
275    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    // Reuse or read the `paths.json` and `index.json` files from the package
284    // directory
285    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    // Error out if this is a noarch python package but the python information is
290    // missing.
291    if index_json.noarch.is_python() && options.python_info.is_none() {
292        return Err(InstallError::MissingPythonInfo);
293    }
294
295    // Parse the `link.json` file and extract entry points from it.
296    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    // Determine whether or not we can use symbolic links
303    let (allow_symbolic_links, allow_hard_links) = tokio::join!(
304        // Determine if we can use symlinks
305        match options.allow_symbolic_links {
306            Some(value) => ready(value).left_future(),
307            None => can_create_symlinks(target_dir).right_future(),
308        },
309        // Determine if we can use hard links
310        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    // Determine the platform to use
324    let platform = options.platform.unwrap_or(Platform::current());
325
326    // compute all path renames
327    let final_paths = compute_paths(&index_json, &paths_json, options.python_info.as_ref());
328
329    // register all paths in the install driver path registry
330    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    // Figure out all the directories that we are going to need
350    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        // Since we store clobbers in the separate directory
362        // (`__clobbers__`) now we have to create all necessary
363        // directories for it as well.
364        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    // Wrap the python info in an `Arc` so we can more easily share it with async
391    // tasks.
392    let python_info = options.python_info.map(Arc::new);
393
394    // Start linking all package files in parallel
395    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            // Spawn a blocking task to link the specific file. We use a blocking task here
407            // because filesystem access is blocking anyway so its more
408            // efficient to group them together in a single blocking call.
409            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            // Construct a `PathsEntry` from the result of the linking operation
437            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                // Only set sha256_in_prefix if it differs from the original sha256
448                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 this package is a noarch python package we also have to create entry
472    // points.
473    //
474    // Be careful with the fact that this code is currently running in parallel with
475    // the linking of individual files.
476    if let Some(link_json) = link_json {
477        // Parse the `link.json` file and extract entry points from it.
478        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        // Get python info
486        let python_info = python_info
487            .clone()
488            .expect("should be safe because its checked above that this contains a value");
489
490        // Create entry points for each listed item. This is different between Windows
491        // and unix because on Windows, two PathEntry's are created whereas on
492        // Linux only one is created.
493        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                // Acquire an IO permit
500                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    // Await the result of all the background tasks. The background tasks are
537    // scheduled in order, however, they can complete in any order. This means
538    // we have to reorder them back into their original order. This is achieved
539    // by waiting to add finished results to the result Vec, if the result
540    // before it has not yet finished. To that end we use a `BinaryHeap` as a
541    // priority queue which will buffer up finished results that finished before
542    // their predecessor.
543    //
544    // What makes this loop special is that it also aborts if any of the returned
545    // results indicate a failure.
546    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                // If this is the next element expected in the sorted list, add it immediately.
553                // This basically means the future finished in order.
554                paths.push(data);
555
556                // By adding a finished future we have to check if there might also be another
557                // future that finished earlier and should also now be added to
558                // the result Vec.
559                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                // Otherwise add it to the out-of-order queue. This means that we still have to
568                // wait for another element before we can add the result to the
569                // ordered list.
570                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/// Given an extracted package archive (`package_dir`), installs its files to
584/// the `target_dir`.
585///
586/// Returns a [`PathsEntry`] for every file that was linked into the target
587/// directory. The entries are ordered in the same order as they appear in the
588/// `paths.json` file of the package.
589#[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    // Determine the target prefix for linking
597    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    // Reuse or read the `paths.json` and `index.json` files from the package
606    // directory
607    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    // Error out if this is a noarch python package but the python information is
623    // missing.
624    if index_json.noarch.is_python() && options.python_info.is_none() {
625        return Err(InstallError::MissingPythonInfo);
626    }
627
628    // Parse the `link.json` file and extract entry points from it.
629    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                            // Its ok if the file is not present.
636                            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    // Determine whether or not we can use symbolic links
653    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    // Determine the platform to use
668    let platform = options.platform.unwrap_or(Platform::current());
669
670    // compute all path renames
671    let final_paths = compute_paths(&index_json, &paths_json, options.python_info.as_ref());
672
673    // register all paths in the install driver path registry
674    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    // Figure out all the directories that we are going to need
690    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        // Iterate over all parent directories and create them if they do not exist.
698        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        // Since we store clobbers in the separate directory
708        // (`__clobbers__`) now we have to create all necessary
709        // directories for it as well.
710        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        // Store the path by directory so we can create them in parallel
721        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 we already (recursively) created the parent directory we can skip this
736        if created_directories
737            .iter()
738            .any(|dir| directory.starts_with(dir))
739        {
740            continue;
741        }
742
743        // can we lock this directory?
744        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            // reflink the whole directory if possible
754            // currently this does not handle noarch packages
755            match reflink_copy::reflink(package_dir.join(&directory), &full_path) {
756                Ok(_) => {
757                    created_directories.insert(directory.clone());
758                    // remove paths that we just reflinked (everything that starts with the directory)
759                    let (matching, non_matching): (HashMap<_, _>, HashMap<_, _>) =
760                        paths_by_directory
761                            .drain()
762                            .partition(|(k, _)| k.starts_with(&directory));
763
764                    // Store matching paths in reflinked_files
765                    reflinked_files.extend(matching);
766                    // Keep non-matching paths in paths_by_directory
767                    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    // Take care of all the reflinked files (macos only)
782    //  - Add them to the paths.json
783    //  - Fix any occurrences of the prefix in the files
784    //  - Rename files that need clobber-renames
785    let mut reflinked_paths_entries = Vec::new();
786    for (parent_dir, files) in reflinked_files {
787        // files that are either in the clobber map or contain a placeholder,
788        // we defer to the regular linking that comes after this block
789        // and re-add them to the paths_by_directory map
790        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                    // No placeholder, no clobbering, so these are none for sure
805                    original_path: None,
806                    sha256_in_prefix: None,
807                    file_mode: None,
808                    prefix_placeholder: None,
809                });
810            }
811        }
812    }
813
814    // Wrap the python info in an `Arc` so we can more easily share it with async
815    // tasks.
816    let python_info = options.python_info;
817
818    // Link the individual files in parallel
819    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                // Construct a `PathsEntry` from the result of the linking operation
858                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                    // Only set sha256_in_prefix if it differs from the original sha256
869                    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 this package is a noarch python package we also have to create entry
893    // points.
894    //
895    // Be careful with the fact that this code is currently running in parallel with
896    // the linking of individual files.
897    if let Some(link_json) = link_json {
898        // Parse the `link.json` file and extract entry points from it.
899        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        // Get python info
907        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        // Create entry points for each listed item. This is different between Windows
914        // and unix because on Windows, two PathEntry's are created whereas on
915        // Linux only one is created.
916        let mut entry_point_paths = if platform.is_windows() {
917            entry_points
918                .into_iter()
919                // .into_par_iter()
920                // .with_min_len(100)
921                .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                // .into_par_iter()
940                // .with_min_len(100)
941                .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
982/// A helper function that reads the `paths.json` file from a package unless it
983/// has already been provided, in which case it is returned immediately.
984async 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
1002/// A helper function that reads the `index.json` file from a package unless it
1003/// has already been provided, in which case it is returned immediately.
1004async 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
1022/// A helper function that reads the `link.json` file from a package unless it
1023/// has already been provided, in which case it is returned immediately.
1024async 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                            // Its ok if the file is not present.
1039                            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
1053/// Returns true if it is possible to create symlinks in the target directory.
1054fn 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
1080/// A helper struct for a `BinaryHeap` to provides ordering to items that are
1081/// otherwise unordered.
1082struct 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        // BinaryHeap is a max heap, so compare backwards here.
1104        other.index.cmp(&self.index)
1105    }
1106}
1107
1108/// Returns true if it is possible to create symlinks in the target directory.
1109async 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
1135/// Returns true if it is possible to create hard links from the target
1136/// directory to the package cache directory.
1137async fn can_create_hardlinks(target_dir: &Prefix, package_dir: &Path) -> bool {
1138    paths_have_same_filesystem(target_dir, package_dir).await
1139}
1140
1141/// Returns true if it is possible to create hard links from the target
1142/// directory to the package cache directory.
1143fn 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/// Returns true if two paths share the same filesystem
1148#[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/// Returns true if two paths share the same filesystem
1158#[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/// Returns true if two paths share the same filesystem
1170#[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/// Returns true if two paths share the same filesystem
1179#[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        // Load a prepared explicit environment file for the current platform.
1209        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        // Load a prepared explicit environment file for the current platform.
1232        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        // Open a package cache in the systems temporary directory with a specific name.
1260        // This allows us to reuse a package cache across multiple invocations
1261        // of this test. Useful if you're debugging.
1262        let package_cache = PackageCache::new(temp_dir().join("rattler").join(cache_name));
1263
1264        // Create an HTTP client we can use to download packages
1265        let client = LazyClient::default();
1266
1267        // Specify python version
1268        let python_version =
1269            PythonInfo::from_version(&Version::from_str("3.11.0").unwrap(), None, platform)
1270                .unwrap();
1271
1272        // Download and install each layer into an environment.
1273        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                    // Populate the cache
1285                    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                    // Install the package to the prefix
1297                    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        // Run the python command and validate the version it outputs
1313        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        // Create package cache
1346        rattler_package_streaming::fs::extract(&package_path, package_dir.path()).unwrap();
1347
1348        let install_driver = InstallDriver::default();
1349
1350        // Link the package
1351        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}