pub mod apple_codesign;
mod clobber_registry;
mod driver;
mod entry_point;
pub mod link;
pub mod link_script;
mod python;
mod transaction;
pub mod unlink;
mod installer;
#[cfg(test)]
mod test_utils;
use std::{
cmp::Ordering,
collections::{binary_heap::PeekMut, BinaryHeap, HashMap, HashSet},
fs,
future::ready,
io::ErrorKind,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
pub use apple_codesign::AppleCodeSignBehavior;
pub use clobber_registry::ClobberMode;
pub use driver::InstallDriver;
use fs_err::tokio as tokio_fs;
use futures::{stream::FuturesUnordered, FutureExt, StreamExt};
pub use installer::{
result_record::InstallationResultRecord, Installer, InstallerError, LinkOptions, Reporter,
};
#[cfg(feature = "indicatif")]
pub use installer::{
DefaultProgressFormatter, IndicatifReporter, IndicatifReporterBuilder, Placement,
ProgressFormatter,
};
use itertools::Itertools;
pub use link::{link_file, LinkFileError, LinkMethod};
pub use python::PythonInfo;
use rattler_conda_types::{
package::{AboutJson, IndexJson, LinkJson, NoArchLinks, PackageFile, PathsEntry, PathsJson},
prefix::Prefix,
prefix_record::{self, LinkType},
Platform,
};
use rayon::{
iter::Either,
prelude::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator},
};
use simple_spawn_blocking::Cancelled;
use tokio::task::JoinError;
use tracing::instrument;
pub use transaction::{Transaction, TransactionError, TransactionOperation};
pub use unlink::{empty_trash, unlink_package};
pub use crate::install::entry_point::{get_windows_launcher, python_entry_point_template};
use crate::install::{
clobber_registry::{ClobberRegistry, CLOBBERS_DIR_NAME},
entry_point::{create_unix_python_entry_point, create_windows_python_entry_point},
};
#[derive(Debug, thiserror::Error)]
pub enum InstallError {
#[error("the operation was cancelled")]
Cancelled,
#[error("failed to read 'paths.json'")]
FailedToReadPathsJson(#[source] std::io::Error),
#[error("failed to read 'index.json'")]
FailedToReadIndexJson(#[source] std::io::Error),
#[error("failed to read 'link.json'")]
FailedToReadLinkJson(#[source] std::io::Error),
#[error("failed to link '{0}'")]
FailedToLink(PathBuf, #[source] LinkFileError),
#[error("failed to create directory '{0}'")]
FailedToCreateDirectory(PathBuf, #[source] std::io::Error),
#[error("target prefix is not UTF-8")]
TargetPrefixIsNotUtf8,
#[error("failed to create target directory")]
FailedToCreateTargetDirectory(#[source] std::io::Error),
#[error("cannot install noarch python package because there is no python version specified")]
MissingPythonInfo,
#[error("failed to create Python entry point")]
FailedToCreatePythonEntryPoint(#[source] std::io::Error),
#[error("failed to post process the environment (unclobbering)")]
PostProcessFailed(#[source] std::io::Error),
}
impl From<Cancelled> for InstallError {
fn from(_: Cancelled) -> Self {
InstallError::Cancelled
}
}
impl From<JoinError> for InstallError {
fn from(err: JoinError) -> Self {
if let Ok(panic) = err.try_into_panic() {
std::panic::resume_unwind(panic)
} else {
InstallError::Cancelled
}
}
}
#[derive(Default, Clone)]
pub struct InstallOptions {
pub target_prefix: Option<PathBuf>,
pub paths_json: Option<PathsJson>,
pub index_json: Option<IndexJson>,
pub link_json: Option<Option<LinkJson>>,
pub allow_symbolic_links: Option<bool>,
pub allow_hard_links: Option<bool>,
pub allow_ref_links: Option<bool>,
pub platform: Option<Platform>,
pub python_info: Option<PythonInfo>,
pub apple_codesign_behavior: AppleCodeSignBehavior,
pub external_symlink_policy: ExternalSymlinkPolicy,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ExternalSymlinkPolicy {
Allow,
#[default]
Warn,
Deny,
}
#[derive(Debug)]
struct LinkPath {
entry: PathsEntry,
computed_path: PathBuf,
clobber_path: Option<PathBuf>,
}
fn modification_time(package_dir: &Path) -> filetime::FileTime {
if let Ok(info_metadata) = fs_err::symlink_metadata(package_dir.join(AboutJson::package_path()))
{
let info_time = filetime::FileTime::from_last_modification_time(&info_metadata);
filetime::FileTime::from_unix_time(info_time.unix_seconds() + 1, info_time.nanoseconds())
} else {
filetime::FileTime::now()
}
}
#[instrument(skip_all, fields(package_dir = % package_dir.display()))]
pub async fn link_package(
package_dir: &Path,
target_dir: &Prefix,
driver: &InstallDriver,
options: InstallOptions,
) -> Result<Vec<prefix_record::PathsEntry>, InstallError> {
let target_prefix = options
.target_prefix
.as_deref()
.unwrap_or(target_dir)
.to_str()
.ok_or(InstallError::TargetPrefixIsNotUtf8)?
.to_owned();
let paths_json = read_paths_json(package_dir, driver, options.paths_json);
let index_json = read_index_json(package_dir, driver, options.index_json);
let (paths_json, index_json) = tokio::try_join!(paths_json, index_json)?;
let modification_time = modification_time(package_dir);
if index_json.noarch.is_python() && options.python_info.is_none() {
return Err(InstallError::MissingPythonInfo);
}
let link_json = if index_json.noarch.is_python() {
read_link_json(package_dir, driver, options.link_json.flatten()).await?
} else {
None
};
let (allow_symbolic_links, allow_hard_links) = tokio::join!(
match options.allow_symbolic_links {
Some(value) => ready(value).left_future(),
None => can_create_symlinks(target_dir).right_future(),
},
match options.allow_hard_links {
Some(value) => ready(value).left_future(),
None => can_create_hardlinks(target_dir, package_dir).right_future(),
},
);
let allow_ref_links = options.allow_ref_links.unwrap_or_else(|| {
match reflink_copy::check_reflink_support(package_dir, target_dir) {
Ok(reflink_copy::ReflinkSupport::Supported) => true,
Ok(reflink_copy::ReflinkSupport::NotSupported) | Err(_) => false,
Ok(reflink_copy::ReflinkSupport::Unknown) => allow_hard_links,
}
});
let platform = options.platform.unwrap_or(Platform::current());
let final_paths = compute_paths(&index_json, &paths_json, options.python_info.as_ref());
let clobber_paths = Arc::new(
driver
.clobber_registry()
.register_paths(&index_json, &final_paths),
);
let final_paths: Vec<LinkPath> = final_paths
.into_iter()
.map(|el| {
let (entry, computed_path) = el;
let clobber_path = clobber_paths.get(&computed_path).cloned();
LinkPath {
entry,
computed_path,
clobber_path,
}
})
.collect();
let mut directories_to_construct = HashSet::new();
for link_path in &final_paths {
let mut current_path = link_path.computed_path.parent();
while let Some(path) = current_path {
if !path.as_os_str().is_empty() && directories_to_construct.insert(path.to_path_buf()) {
current_path = path.parent();
} else {
break;
}
}
let clobber_path = link_path.clobber_path.as_ref();
let mut current_clobber_path = clobber_path.and_then(|p| p.parent());
while let Some(path) = current_clobber_path {
if !path.as_os_str().is_empty() && directories_to_construct.insert(path.to_path_buf()) {
current_clobber_path = path.parent();
} else {
break;
}
}
}
let directories_target_dir = target_dir.path().to_path_buf();
driver
.run_blocking_io_task(move || {
for directory in directories_to_construct.into_iter().sorted() {
let full_path = directories_target_dir.join(directory);
match fs::create_dir(&full_path) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::AlreadyExists => (),
Err(e) => return Err(InstallError::FailedToCreateDirectory(full_path, e)),
}
}
Ok(())
})
.await?;
let python_info = options.python_info.map(Arc::new);
let mut pending_futures = FuturesUnordered::new();
let mut number_of_paths_entries = 0;
for link_path in final_paths {
let entry = link_path.entry;
let package_dir = package_dir.to_owned();
let target_dir = target_dir.to_owned();
let target_prefix = target_prefix.clone();
let install_future = async move {
let _permit = driver.acquire_io_permit().await;
let cloned_entry = entry.clone();
let is_clobber = link_path.clobber_path.is_some();
let result = match tokio::task::spawn_blocking(move || {
link_file(
&cloned_entry,
link_path.clobber_path.unwrap_or(link_path.computed_path),
&package_dir,
&target_dir,
&target_prefix,
allow_symbolic_links && !cloned_entry.no_link,
allow_hard_links && !cloned_entry.no_link,
allow_ref_links && !cloned_entry.no_link,
platform,
options.apple_codesign_behavior,
modification_time,
options.external_symlink_policy,
)
})
.await
.map_err(JoinError::try_into_panic)
{
Ok(Ok(linked_file)) => linked_file,
Ok(Err(e)) => {
return Err(InstallError::FailedToLink(entry.relative_path.clone(), e));
}
Err(Ok(payload)) => std::panic::resume_unwind(payload),
Err(Err(_err)) => return Err(InstallError::Cancelled),
};
let paths_entry = prefix_record::PathsEntry {
relative_path: result.relative_path,
original_path: if is_clobber {
Some(entry.relative_path)
} else {
None
},
path_type: entry.path_type.into(),
no_link: entry.no_link,
sha256: entry.sha256,
sha256_in_prefix: if Some(result.sha256) == entry.sha256 {
None
} else {
Some(result.sha256)
},
size_in_bytes: Some(result.file_size),
file_mode: match result.method {
LinkMethod::Patched(file_mode) => Some(file_mode),
_ => None,
},
prefix_placeholder: entry
.prefix_placeholder
.as_ref()
.map(|p| p.placeholder.clone()),
};
Ok(vec![(number_of_paths_entries, paths_entry)])
};
pending_futures.push(install_future.boxed());
number_of_paths_entries += 1;
}
if let Some(link_json) = link_json {
let entry_points = match link_json.noarch {
NoArchLinks::Python(entry_points) => entry_points.entry_points,
NoArchLinks::Generic => {
unreachable!("we only use link.json for noarch: python packages")
}
};
let python_info = python_info
.clone()
.expect("should be safe because its checked above that this contains a value");
for entry_point in entry_points {
let python_info = python_info.clone();
let target_dir = target_dir.to_owned();
let target_prefix = target_prefix.clone();
let entry_point_fut = async move {
let _permit = driver.acquire_io_permit().await;
let entries = if platform.is_windows() {
match create_windows_python_entry_point(
&target_dir,
&target_prefix,
&entry_point,
&python_info,
&platform,
) {
Ok([a, b]) => vec![
(number_of_paths_entries, a),
(number_of_paths_entries + 1, b),
],
Err(e) => return Err(InstallError::FailedToCreatePythonEntryPoint(e)),
}
} else {
match create_unix_python_entry_point(
&target_dir,
&target_prefix,
&entry_point,
&python_info,
) {
Ok(a) => vec![(number_of_paths_entries, a)],
Err(e) => return Err(InstallError::FailedToCreatePythonEntryPoint(e)),
}
};
Ok(entries)
};
pending_futures.push(entry_point_fut.boxed());
number_of_paths_entries += if platform.is_windows() { 2 } else { 1 };
}
}
let mut paths = Vec::with_capacity(number_of_paths_entries);
let mut out_of_order_queue =
BinaryHeap::<OrderWrapper<prefix_record::PathsEntry>>::with_capacity(100);
while let Some(link_result) = pending_futures.next().await {
for (index, data) in link_result? {
if index == paths.len() {
paths.push(data);
while let Some(next_output) = out_of_order_queue.peek_mut() {
if next_output.index == paths.len() {
paths.push(PeekMut::pop(next_output).data);
} else {
break;
}
}
} else {
out_of_order_queue.push(OrderWrapper { index, data });
}
}
}
debug_assert_eq!(
paths.len(),
paths.capacity(),
"some futures where not added to the result"
);
Ok(paths)
}
#[instrument(skip_all, fields(package_dir = % package_dir.display()))]
pub fn link_package_sync(
package_dir: &Path,
target_dir: &Prefix,
clobber_registry: Arc<Mutex<ClobberRegistry>>,
options: InstallOptions,
) -> Result<(Vec<prefix_record::PathsEntry>, LinkType), InstallError> {
let target_prefix = options
.target_prefix
.as_deref()
.unwrap_or(target_dir)
.to_str()
.ok_or(InstallError::TargetPrefixIsNotUtf8)?
.to_owned();
let paths_json = options.paths_json.map_or_else(
|| {
PathsJson::from_package_directory_with_deprecated_fallback(package_dir)
.map_err(InstallError::FailedToReadPathsJson)
},
Ok,
)?;
let index_json = options.index_json.map_or_else(
|| {
IndexJson::from_package_directory(package_dir)
.map_err(InstallError::FailedToReadIndexJson)
},
Ok,
)?;
let modification_time = modification_time(package_dir);
if index_json.noarch.is_python() && options.python_info.is_none() {
return Err(InstallError::MissingPythonInfo);
}
let link_json = if index_json.noarch.is_python() {
options.link_json.flatten().map_or_else(
|| {
LinkJson::from_package_directory(package_dir)
.map_or_else(
|e| {
if e.kind() == ErrorKind::NotFound {
Ok(None)
} else {
Err(e)
}
},
|link_json| Ok(Some(link_json)),
)
.map_err(InstallError::FailedToReadLinkJson)
},
|value| Ok(Some(value)),
)?
} else {
None
};
let allow_symbolic_links = options
.allow_symbolic_links
.unwrap_or_else(|| can_create_symlinks_sync(target_dir));
let allow_hard_links = options
.allow_hard_links
.unwrap_or_else(|| can_create_hardlinks_sync(target_dir, package_dir));
let link_type = if allow_hard_links {
LinkType::HardLink
} else {
LinkType::Copy
};
let mut allow_ref_links = options.allow_ref_links.unwrap_or_else(|| {
match reflink_copy::check_reflink_support(package_dir, target_dir) {
Ok(reflink_copy::ReflinkSupport::Supported) => true,
Ok(reflink_copy::ReflinkSupport::NotSupported) | Err(_) => false,
Ok(reflink_copy::ReflinkSupport::Unknown) => allow_hard_links,
}
});
let platform = options.platform.unwrap_or(Platform::current());
let final_paths = compute_paths(&index_json, &paths_json, options.python_info.as_ref());
let clobber_paths = clobber_registry
.lock()
.unwrap()
.register_paths(&index_json, &final_paths);
let final_paths = final_paths.into_iter().map(|el| {
let (entry, computed_path) = el;
let clobber_path = clobber_paths.get(&computed_path).cloned();
LinkPath {
entry,
computed_path,
clobber_path,
}
});
let mut directories_to_construct = HashSet::new();
let mut paths_by_directory = HashMap::new();
for link_path in final_paths {
let Some(entry_parent) = link_path.computed_path.parent() else {
continue;
};
let mut current_path = Some(entry_parent);
while let Some(path) = current_path {
if !path.as_os_str().is_empty() && directories_to_construct.insert(path.to_path_buf()) {
current_path = path.parent();
} else {
break;
}
}
let clobber_path = link_path.clobber_path.as_ref();
let mut current_path = clobber_path.and_then(|p| p.parent());
while let Some(path) = current_path {
if !path.as_os_str().is_empty() && directories_to_construct.insert(path.to_path_buf()) {
current_path = path.parent();
} else {
break;
}
}
paths_by_directory
.entry(entry_parent.to_path_buf())
.or_insert_with(Vec::new)
.push(link_path);
}
let mut created_directories = HashSet::new();
let mut reflinked_files = HashMap::new();
for directory in directories_to_construct
.into_iter()
.sorted_by(|a, b| a.components().count().cmp(&b.components().count()))
{
let full_path = target_dir.path().join(&directory);
if created_directories
.iter()
.any(|dir| directory.starts_with(dir))
{
continue;
}
if full_path.exists() {
continue;
}
if allow_ref_links
&& cfg!(target_os = "macos")
&& !directory.starts_with(CLOBBERS_DIR_NAME)
&& !index_json.noarch.is_python()
{
match reflink_copy::reflink(package_dir.join(&directory), &full_path) {
Ok(_) => {
created_directories.insert(directory.clone());
let (matching, non_matching): (HashMap<_, _>, HashMap<_, _>) =
paths_by_directory
.drain()
.partition(|(k, _)| k.starts_with(&directory));
reflinked_files.extend(matching);
paths_by_directory = non_matching;
}
Err(e) if e.kind() == ErrorKind::AlreadyExists => (),
Err(_) => {
allow_ref_links = false;
match fs::create_dir(&full_path) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::AlreadyExists => (),
Err(e) => return Err(InstallError::FailedToCreateDirectory(full_path, e)),
}
}
}
} else {
match fs::create_dir(&full_path) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::AlreadyExists => (),
Err(e) => return Err(InstallError::FailedToCreateDirectory(full_path, e)),
}
}
}
let mut reflinked_paths_entries = Vec::new();
for (parent_dir, files) in reflinked_files {
for link_path in files {
if link_path.clobber_path.is_some() || link_path.entry.prefix_placeholder.is_some() {
paths_by_directory
.entry(parent_dir.clone())
.or_insert_with(Vec::new)
.push(link_path);
} else {
let entry = link_path.entry;
reflinked_paths_entries.push(prefix_record::PathsEntry {
relative_path: entry.relative_path,
path_type: entry.path_type.into(),
no_link: entry.no_link,
sha256: entry.sha256,
size_in_bytes: entry.size_in_bytes,
original_path: None,
sha256_in_prefix: None,
file_mode: None,
prefix_placeholder: None,
});
}
}
}
let python_info = options.python_info;
let link_target_prefix = target_prefix.clone();
let package_dir = package_dir.to_path_buf();
let mut paths = paths_by_directory
.into_values()
.collect_vec()
.into_par_iter()
.with_min_len(100)
.flat_map(move |entries_in_subdir| {
let mut path_entries = Vec::with_capacity(entries_in_subdir.len());
for link_path in entries_in_subdir {
let entry = link_path.entry;
let is_clobber = link_path.clobber_path.is_some();
let link_result = link_file(
&entry,
link_path
.clobber_path
.unwrap_or(link_path.computed_path.clone()),
&package_dir,
target_dir,
&link_target_prefix,
allow_symbolic_links && !entry.no_link,
allow_hard_links && !entry.no_link,
allow_ref_links && !entry.no_link,
platform,
options.apple_codesign_behavior,
modification_time,
options.external_symlink_policy,
);
let result = match link_result {
Ok(linked_file) => linked_file,
Err(e) => {
return vec![Err(InstallError::FailedToLink(
entry.relative_path.clone(),
e,
))];
}
};
path_entries.push(Ok(prefix_record::PathsEntry {
relative_path: result.relative_path,
original_path: if is_clobber {
Some(link_path.computed_path)
} else {
None
},
path_type: entry.path_type.into(),
no_link: entry.no_link,
sha256: entry.sha256,
sha256_in_prefix: if Some(result.sha256) == entry.sha256 {
None
} else {
Some(result.sha256)
},
size_in_bytes: Some(result.file_size),
file_mode: match result.method {
LinkMethod::Patched(file_mode) => Some(file_mode),
_ => None,
},
prefix_placeholder: entry
.prefix_placeholder
.as_ref()
.map(|p| p.placeholder.clone()),
}));
}
path_entries
})
.collect::<Result<Vec<_>, _>>()?;
paths.extend(reflinked_paths_entries);
if let Some(link_json) = link_json {
let entry_points = match link_json.noarch {
NoArchLinks::Python(entry_points) => entry_points.entry_points,
NoArchLinks::Generic => {
unreachable!("we only use link.json for noarch: python packages")
}
};
let python_info = python_info
.clone()
.expect("should be safe because its checked above that this contains a value");
let target_prefix = target_prefix.clone();
let mut entry_point_paths = if platform.is_windows() {
entry_points
.into_iter()
.flat_map(move |entry_point| {
match create_windows_python_entry_point(
target_dir,
&target_prefix,
&entry_point,
&python_info,
&platform,
) {
Ok([a, b]) => Either::Left([Ok(a), Ok(b)].into_iter()),
Err(e) => Either::Right(std::iter::once(Err(
InstallError::FailedToCreatePythonEntryPoint(e),
))),
}
})
.collect::<Result<Vec<_>, _>>()?
} else {
entry_points
.into_iter()
.map(move |entry_point| {
match create_unix_python_entry_point(
target_dir,
&target_prefix,
&entry_point,
&python_info,
) {
Ok(a) => Ok(a),
Err(e) => Err(InstallError::FailedToCreatePythonEntryPoint(e)),
}
})
.collect::<Result<_, _>>()?
};
paths.append(&mut entry_point_paths);
};
Ok((paths, link_type))
}
fn compute_paths(
index_json: &IndexJson,
paths_json: &PathsJson,
python_info: Option<&PythonInfo>,
) -> Vec<(rattler_conda_types::package::PathsEntry, PathBuf)> {
let mut final_paths = Vec::with_capacity(paths_json.paths.len());
for entry in &paths_json.paths {
let path = if index_json.noarch.is_python() {
python_info
.unwrap()
.get_python_noarch_target_path(&entry.relative_path)
.to_path_buf()
} else {
entry.relative_path.clone()
};
final_paths.push((entry.clone(), path));
}
final_paths
}
async fn read_paths_json(
package_dir: &Path,
driver: &InstallDriver,
paths_json: Option<PathsJson>,
) -> Result<PathsJson, InstallError> {
if let Some(paths_json) = paths_json {
Ok(paths_json)
} else {
let package_dir = package_dir.to_owned();
driver
.run_blocking_io_task(move || {
PathsJson::from_package_directory_with_deprecated_fallback(&package_dir)
.map_err(InstallError::FailedToReadPathsJson)
})
.await
}
}
async fn read_index_json(
package_dir: &Path,
driver: &InstallDriver,
index_json: Option<IndexJson>,
) -> Result<IndexJson, InstallError> {
if let Some(index) = index_json {
Ok(index)
} else {
let package_dir = package_dir.to_owned();
driver
.run_blocking_io_task(move || {
IndexJson::from_package_directory(package_dir)
.map_err(InstallError::FailedToReadIndexJson)
})
.await
}
}
async fn read_link_json(
package_dir: &Path,
driver: &InstallDriver,
index_json: Option<LinkJson>,
) -> Result<Option<LinkJson>, InstallError> {
if let Some(index) = index_json {
Ok(Some(index))
} else {
let package_dir = package_dir.to_owned();
driver
.run_blocking_io_task(move || {
LinkJson::from_package_directory(package_dir)
.map_or_else(
|e| {
if e.kind() == ErrorKind::NotFound {
Ok(None)
} else {
Err(e)
}
},
|link_json| Ok(Some(link_json)),
)
.map_err(InstallError::FailedToReadLinkJson)
})
.await
}
}
fn can_create_symlinks_sync(target_dir: &Prefix) -> bool {
let uuid = uuid::Uuid::new_v4();
let symlink_path = target_dir.path().join(format!("symtest_{uuid}"));
#[cfg(windows)]
let result = std::os::windows::fs::symlink_file("./", &symlink_path);
#[cfg(unix)]
let result = fs_err::os::unix::fs::symlink("./", &symlink_path);
match result {
Ok(_) => {
if let Err(e) = fs_err::remove_file(&symlink_path) {
tracing::warn!(
"failed to delete temporary file '{}': {e}",
symlink_path.display()
);
}
true
}
Err(e) => {
tracing::debug!(
"failed to create symlink in target directory: {e}. Disabling use of symlinks."
);
false
}
}
}
struct OrderWrapper<T> {
index: usize,
data: T,
}
impl<T> PartialEq for OrderWrapper<T> {
fn eq(&self, other: &Self) -> bool {
self.index == other.index
}
}
impl<T> Eq for OrderWrapper<T> {}
impl<T> PartialOrd for OrderWrapper<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<T> Ord for OrderWrapper<T> {
fn cmp(&self, other: &Self) -> Ordering {
other.index.cmp(&self.index)
}
}
async fn can_create_symlinks(target_dir: &Prefix) -> bool {
let uuid = uuid::Uuid::new_v4();
let symlink_path = target_dir.path().join(format!("symtest_{uuid}"));
#[cfg(windows)]
let result = tokio_fs::symlink_file("./", &symlink_path).await;
#[cfg(unix)]
let result = tokio_fs::symlink("./", &symlink_path).await;
match result {
Ok(_) => {
if let Err(e) = tokio_fs::remove_file(&symlink_path).await {
tracing::warn!(
"failed to delete temporary file '{}': {e}",
symlink_path.display()
);
}
true
}
Err(e) => {
tracing::debug!(
"failed to create symlink in target directory: {e}. Disabling use of symlinks."
);
false
}
}
}
async fn can_create_hardlinks(target_dir: &Prefix, package_dir: &Path) -> bool {
paths_have_same_filesystem(target_dir, package_dir).await
}
fn can_create_hardlinks_sync(target_dir: &Prefix, package_dir: &Path) -> bool {
paths_have_same_filesystem_sync(target_dir.path(), package_dir)
}
#[cfg(unix)]
async fn paths_have_same_filesystem(a: &Prefix, b: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
match tokio::join!(tokio_fs::metadata(a.path()), tokio_fs::metadata(b)) {
(Ok(a), Ok(b)) => a.dev() == b.dev(),
_ => false,
}
}
#[cfg(unix)]
fn paths_have_same_filesystem_sync(a: &Path, b: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
let a = std::fs::metadata(a);
let b = std::fs::metadata(b);
match (a, b) {
(Ok(a), Ok(b)) => a.dev() == b.dev(),
_ => false,
}
}
#[cfg(not(unix))]
async fn paths_have_same_filesystem(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(a), Ok(b)) => a.components().next() == b.components().next(),
_ => false,
}
}
#[cfg(not(unix))]
fn paths_have_same_filesystem_sync(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(a), Ok(b)) => a.components().next() == b.components().next(),
_ => false,
}
}
#[cfg(test)]
mod test {
use std::{env::temp_dir, process::Command, str::FromStr};
use crate::{
get_test_data_dir,
install::{link_package, InstallDriver, InstallOptions, Prefix, PythonInfo},
package_cache::PackageCache,
};
use futures::{stream, StreamExt};
use rattler_conda_types::{
package::CondaArchiveIdentifier, ExplicitEnvironmentSpec, Platform, Version,
};
use rattler_lock::LockFile;
use rattler_networking::LazyClient;
use tempfile::tempdir;
use url::Url;
#[tracing_test::traced_test]
#[tokio::test]
pub async fn test_explicit_lock() {
let current_platform = Platform::current();
let explicit_env_path =
get_test_data_dir().join(format!("python/explicit-env-{current_platform}.txt"));
let env = ExplicitEnvironmentSpec::from_path(&explicit_env_path).unwrap();
assert_eq!(
env.platform,
Some(current_platform),
"the platform for which the explicit lock file was created does not match the current platform"
);
test_install_python(
env.packages.into_iter().map(|p| p.url),
"explicit",
current_platform,
)
.await;
}
#[tracing_test::traced_test]
#[tokio::test]
pub async fn test_conda_lock() {
let lock_path = get_test_data_dir().join("conda-lock/v4/python-lock.yml");
let lock = LockFile::from_path(&lock_path).unwrap();
let current_platform = Platform::current();
let lock_env = lock
.default_environment()
.expect("no default environment in lock file");
let Some(packages) = lock_env.packages(current_platform) else {
panic!(
"the platform for which the explicit lock file was created does not match the current platform"
)
};
test_install_python(
packages.filter_map(|p| p.as_conda()?.location().as_url().cloned()),
"conda-lock",
current_platform,
)
.await;
}
pub async fn test_install_python(
urls: impl Iterator<Item = Url>,
cache_name: &str,
platform: Platform,
) {
let package_cache = PackageCache::new(temp_dir().join("rattler").join(cache_name));
let client = LazyClient::default();
let python_version =
PythonInfo::from_version(&Version::from_str("3.11.0").unwrap(), None, platform)
.unwrap();
let install_driver = InstallDriver::default();
let target_dir = tempdir().unwrap();
let prefix_path = Prefix::create(target_dir.path()).unwrap();
stream::iter(urls)
.for_each_concurrent(Some(50), |package_url| {
let client = client.clone();
let package_cache = &package_cache;
let install_driver = &install_driver;
let python_version = &python_version;
let prefix_path = prefix_path.clone();
async move {
let package_info = CondaArchiveIdentifier::try_from_url(&package_url).unwrap();
let package_cache_lock = package_cache
.get_or_fetch_from_url(
package_info,
package_url.clone(),
client.clone(),
None,
)
.await
.unwrap();
link_package(
package_cache_lock.path(),
&prefix_path,
install_driver,
InstallOptions {
python_info: Some(python_version.clone()),
..InstallOptions::default()
},
)
.await
.unwrap();
}
})
.await;
let python_path = if Platform::current().is_windows() {
"python.exe"
} else {
"bin/python"
};
let python_version = Command::new(target_dir.path().join(python_path))
.arg("--version")
.output()
.unwrap();
assert!(python_version.status.success());
assert_eq!(
String::from_utf8_lossy(&python_version.stdout).trim(),
"Python 3.11.0"
);
}
#[tracing_test::traced_test]
#[tokio::test]
async fn test_prefix_paths() {
let environment_dir = tempfile::TempDir::new().unwrap();
let package_dir = tempfile::TempDir::new().unwrap();
let package_path = tools::download_and_cache_file_async(
"https://conda.anaconda.org/conda-forge/win-64/ruff-0.0.171-py310h298983d_0.conda"
.parse()
.unwrap(),
"25c755b97189ee066576b4ae3999d5e7ff4406d236b984742194e63941838dcd",
)
.await
.unwrap();
rattler_package_streaming::fs::extract(&package_path, package_dir.path()).unwrap();
let install_driver = InstallDriver::default();
let paths = link_package(
package_dir.path(),
&Prefix::create(environment_dir.path()).unwrap(),
&install_driver,
InstallOptions::default(),
)
.await
.unwrap();
insta::assert_yaml_snapshot!(paths);
}
}