use crate::project::grouped_environment::GroupedEnvironmentName;
use crate::{
consts, install, install_pypi,
lock_file::UpdateLockFileOptions,
prefix::Prefix,
progress,
project::{
grouped_environment::GroupedEnvironment,
manifest::{EnvironmentName, SystemRequirements},
virtual_packages::verify_current_platform_has_required_virtual_packages,
Environment,
},
Project,
};
use indexmap::IndexMap;
use miette::IntoDiagnostic;
use rattler::{
install::{PythonInfo, Transaction},
package_cache::PackageCache,
};
use rattler_conda_types::{Channel, Platform, PrefixRecord, RepoDataRecord};
use rattler_lock::{PypiPackageData, PypiPackageEnvironmentData};
use rattler_repodata_gateway::sparse::SparseRepoData;
use reqwest_middleware::ClientWithMiddleware;
use rip::{index::PackageDb, resolve::solve_options::SDistResolution};
use std::{collections::HashMap, io::ErrorKind, path::Path, sync::Arc};
pub fn verify_prefix_location_unchanged(prefix_file: &Path) -> miette::Result<()> {
match std::fs::read_to_string(prefix_file) {
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
Err(e) => Err(e).into_diagnostic(),
Ok(p) if prefix_file.starts_with(&p) => Ok(()),
Ok(p) => Err(miette::miette!(
"the project location seems to be change from `{}` to `{}`, this is not allowed.\
\nPlease remove the `{}` folder and run again",
p,
prefix_file
.parent()
.expect("prefix_file should always be a file")
.display(),
consts::PIXI_DIR
)),
}
}
fn create_prefix_location_file(prefix_file: &Path) -> miette::Result<()> {
let parent = prefix_file
.parent()
.ok_or_else(|| miette::miette!("cannot find parent of '{}'", prefix_file.display()))?;
if parent.exists() {
let contents = parent.to_str().ok_or_else(|| {
miette::miette!("failed to convert path to str: '{}'", parent.display())
})?;
std::fs::write(prefix_file, contents).into_diagnostic()?;
}
Ok(())
}
pub fn sanity_check_project(project: &Project) -> miette::Result<()> {
verify_prefix_location_unchanged(
project
.default_environment()
.dir()
.join(consts::PREFIX_FILE_NAME)
.as_path(),
)?;
verify_current_platform_has_required_virtual_packages(&project.default_environment())
.into_diagnostic()?;
let old_pixi_env_dir = project.pixi_dir().join("env");
if old_pixi_env_dir.exists() {
tracing::warn!(
"The `{}` folder is deprecated, please remove it as we now use the `{}` folder",
old_pixi_env_dir.display(),
consts::ENVIRONMENTS_DIR
);
}
Ok(())
}
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
pub enum LockFileUsage {
#[default]
Update,
Locked,
Frozen,
}
impl LockFileUsage {
pub fn allows_lock_file_updates(self) -> bool {
match self {
LockFileUsage::Update => true,
LockFileUsage::Locked | LockFileUsage::Frozen => false,
}
}
pub fn should_check_if_out_of_date(self) -> bool {
match self {
LockFileUsage::Update | LockFileUsage::Locked => true,
LockFileUsage::Frozen => false,
}
}
}
pub async fn get_up_to_date_prefix(
environment: &Environment<'_>,
lock_file_usage: LockFileUsage,
mut no_install: bool,
existing_repo_data: IndexMap<(Channel, Platform), SparseRepoData>,
) -> miette::Result<Prefix> {
let current_platform = Platform::current();
let project = environment.project();
if !no_install && !environment.platforms().contains(¤t_platform) {
tracing::warn!("Not installing dependency on current platform: ({current_platform}) as it is not part of this project's supported platforms.");
no_install = true;
}
sanity_check_project(project)?;
let mut lock_file = project
.up_to_date_lock_file(UpdateLockFileOptions {
existing_repo_data,
lock_file_usage,
no_install,
..UpdateLockFileOptions::default()
})
.await?;
if no_install {
Ok(Prefix::new(environment.dir()))
} else {
lock_file.prefix(environment).await
}
}
#[allow(clippy::too_many_arguments)]
pub async fn update_prefix_pypi(
environment_name: &EnvironmentName,
prefix: &Prefix,
platform: Platform,
package_db: Arc<PackageDb>,
conda_records: &[RepoDataRecord],
pypi_records: &[(PypiPackageData, PypiPackageEnvironmentData)],
status: &PythonStatus,
system_requirements: &SystemRequirements,
sdist_resolution: SDistResolution,
env_variables: HashMap<String, String>,
) -> miette::Result<()> {
install_pypi::remove_old_python_distributions(prefix, platform, status)?;
progress::await_in_progress(
format!(
"updating pypi package in '{}'",
environment_name.fancy_display()
),
|_| {
install_pypi::update_python_distributions(
package_db,
prefix,
conda_records,
pypi_records,
platform,
status,
system_requirements,
sdist_resolution,
env_variables,
)
},
)
.await
}
#[derive(Clone)]
pub enum PythonStatus {
Changed { old: PythonInfo, new: PythonInfo },
Unchanged(PythonInfo),
Removed { old: PythonInfo },
Added { new: PythonInfo },
DoesNotExist,
}
impl PythonStatus {
pub fn from_transaction(transaction: &Transaction<PrefixRecord, RepoDataRecord>) -> Self {
match (
transaction.current_python_info.as_ref(),
transaction.python_info.as_ref(),
) {
(Some(old), Some(new)) if old.short_version != new.short_version => {
PythonStatus::Changed {
old: old.clone(),
new: new.clone(),
}
}
(Some(_), Some(new)) => PythonStatus::Unchanged(new.clone()),
(None, Some(new)) => PythonStatus::Added { new: new.clone() },
(Some(old), None) => PythonStatus::Removed { old: old.clone() },
(None, None) => PythonStatus::DoesNotExist,
}
}
pub fn current_info(&self) -> Option<&PythonInfo> {
match self {
PythonStatus::Changed { new, .. }
| PythonStatus::Unchanged(new)
| PythonStatus::Added { new } => Some(new),
PythonStatus::Removed { .. } | PythonStatus::DoesNotExist => None,
}
}
pub fn location(&self) -> Option<&Path> {
Some(&self.current_info()?.path)
}
}
pub async fn update_prefix_conda(
environment_name: GroupedEnvironmentName,
prefix: &Prefix,
package_cache: Arc<PackageCache>,
authenticated_client: ClientWithMiddleware,
installed_packages: Vec<PrefixRecord>,
repodata_records: &[RepoDataRecord],
platform: Platform,
) -> miette::Result<PythonStatus> {
let transaction = Transaction::from_current_and_desired(
installed_packages.clone(),
repodata_records.to_owned(),
platform,
)
.into_diagnostic()?;
if !transaction.operations.is_empty() {
progress::await_in_progress(
format!(
"updating packages in '{}'",
environment_name.fancy_display()
),
|pb| async {
install::execute_transaction(
package_cache,
&transaction,
&installed_packages,
prefix.root().to_path_buf(),
authenticated_client,
pb,
)
.await
},
)
.await?;
}
create_prefix_location_file(&prefix.root().join(consts::PREFIX_FILE_NAME))?;
Ok(PythonStatus::from_transaction(&transaction))
}
pub type PerEnvironment<'p, T> = HashMap<Environment<'p>, T>;
pub type PerGroup<'p, T> = HashMap<GroupedEnvironment<'p>, T>;
pub type PerEnvironmentAndPlatform<'p, T> = PerEnvironment<'p, HashMap<Platform, T>>;
pub type PerGroupAndPlatform<'p, T> = PerGroup<'p, HashMap<Platform, T>>;