diff --git a/Cargo.lock b/Cargo.lock index 42ca9b5458..adb0d4f65d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4758,6 +4758,7 @@ dependencies = [ "itertools 0.14.0", "miette 7.6.0", "minijinja", + "pep508_rs", "pixi_config", "pixi_consts", "pixi_core", diff --git a/Cargo.toml b/Cargo.toml index 742c3714fa..2ffa2f3db3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,6 @@ pixi_progress = { path = "crates/pixi_progress" } pixi_pypi_spec = { path = "crates/pixi_pypi_spec" } pixi_python_status = { path = "crates/pixi_python_status" } pixi_record = { path = "crates/pixi_record" } -pixi_variant = { path = "crates/pixi_variant" } pixi_reporters = { path = "crates/pixi_reporters" } pixi_spec = { path = "crates/pixi_spec" } pixi_spec_containers = { path = "crates/pixi_spec_containers" } @@ -114,6 +113,7 @@ pixi_url = { path = "crates/pixi_url" } pixi_utils = { path = "crates/pixi_utils", default-features = false } pixi_uv_context = { path = "crates/pixi_uv_context" } pixi_uv_conversions = { path = "crates/pixi_uv_conversions" } +pixi_variant = { path = "crates/pixi_variant" } pypi_mapping = { path = "crates/pypi_mapping" } pypi_modifiers = { path = "crates/pypi_modifiers" } pyproject-toml = "0.13.7" diff --git a/crates/pixi_api/Cargo.toml b/crates/pixi_api/Cargo.toml index b01a90525d..207d4e6c6c 100644 --- a/crates/pixi_api/Cargo.toml +++ b/crates/pixi_api/Cargo.toml @@ -22,6 +22,7 @@ indexmap = { workspace = true } itertools = { workspace = true } miette = { workspace = true } minijinja = { workspace = true } +pep508_rs = { workspace = true } pixi_config = { workspace = true } pixi_consts = { workspace = true } pixi_core = { workspace = true } diff --git a/crates/pixi_api/src/context.rs b/crates/pixi_api/src/context.rs index d76feaac4a..1b5d567d44 100644 --- a/crates/pixi_api/src/context.rs +++ b/crates/pixi_api/src/context.rs @@ -2,25 +2,29 @@ use std::collections::HashMap; use indexmap::{IndexMap, IndexSet}; use miette::IntoDiagnostic; -use pixi_core::workspace::WorkspaceMut; +use pixi_core::workspace::{PypiDeps, UpdateDeps, WorkspaceMut}; use pixi_core::{Workspace, environment::LockFileUsage}; use pixi_manifest::{ - EnvironmentName, Feature, FeatureName, PrioritizedChannel, TargetSelector, Task, TaskName, + EnvironmentName, Feature, FeatureName, PrioritizedChannel, SpecType, TargetSelector, Task, + TaskName, }; use pixi_pypi_spec::{PixiPypiSpec, PypiPackageName}; use pixi_spec::PixiSpec; use rattler_conda_types::{Channel, MatchSpec, PackageName, Platform, RepoDataRecord}; use crate::interface::Interface; -use crate::workspace::{InitOptions, ReinstallOptions}; +use crate::workspace::add::GitOptions; +use crate::workspace::{DependencyOptions, InitOptions, ReinstallOptions}; pub struct DefaultContext { - interface: I, + _interface: I, } impl DefaultContext { pub fn new(interface: I) -> Self { - Self { interface } + Self { + _interface: interface, + } } /// Returns all matching package versions sorted by version @@ -30,14 +34,7 @@ impl DefaultContext { channels: IndexSet, platform: Platform, ) -> miette::Result>> { - crate::workspace::search::search_exact( - &self.interface, - None, - match_spec, - channels, - platform, - ) - .await + crate::workspace::search::search_exact(None, match_spec, channels, platform).await } /// Returns all matching packages with their latest versions @@ -47,8 +44,7 @@ impl DefaultContext { channels: IndexSet, platform: Platform, ) -> miette::Result>> { - crate::workspace::search::search_wildcard(&self.interface, None, search, channels, platform) - .await + crate::workspace::search::search_wildcard(None, search, channels, platform).await } } @@ -140,11 +136,85 @@ impl WorkspaceContext { .await } + pub async fn add_conda_deps( + &self, + specs: IndexMap, + spec_type: SpecType, + dep_options: DependencyOptions, + git_options: GitOptions, + ) -> miette::Result> { + Box::pin(crate::workspace::add::add_conda_dep( + self.workspace_mut()?, + specs, + spec_type, + dep_options, + git_options, + )) + .await + } + + pub async fn add_pypi_deps( + &self, + pypi_deps: PypiDeps, + editable: bool, + options: DependencyOptions, + ) -> miette::Result> { + Box::pin(crate::workspace::add::add_pypi_dep( + self.workspace_mut()?, + pypi_deps, + editable, + options, + )) + .await + } + + pub async fn remove_conda_deps( + &self, + specs: IndexMap, + spec_type: SpecType, + dep_options: DependencyOptions, + ) -> miette::Result<()> { + Box::pin(crate::workspace::remove::remove_conda_deps( + self.workspace_mut()?, + specs, + spec_type, + dep_options, + )) + .await + } + + pub async fn remove_pypi_deps( + &self, + pypi_deps: PypiDeps, + options: DependencyOptions, + ) -> miette::Result<()> { + Box::pin(crate::workspace::remove::remove_pypi_deps( + self.workspace_mut()?, + pypi_deps, + options, + )) + .await + } + + pub async fn reinstall( + &self, + options: ReinstallOptions, + lock_file_usage: LockFileUsage, + ) -> miette::Result<()> { + crate::workspace::reinstall::reinstall( + &self.interface, + &self.workspace, + options, + lock_file_usage, + ) + .await + } + pub async fn list_tasks( &self, environment: Option, ) -> miette::Result>> { - crate::workspace::task::list_tasks(&self.interface, &self.workspace, environment).await + crate::workspace::task::list_tasks(&self.workspace, environment).await } pub async fn add_task( @@ -197,21 +267,6 @@ impl WorkspaceContext { .await } - pub async fn reinstall( - &self, - options: ReinstallOptions, - lock_file_usage: LockFileUsage, - ) -> miette::Result<()> { - crate::workspace::reinstall::reinstall( - &self.interface, - &self.workspace, - options, - lock_file_usage, - ) - .await - } - - /// Returns all matching package versions sorted by version pub async fn search_exact( &self, match_spec: MatchSpec, @@ -219,7 +274,6 @@ impl WorkspaceContext { platform: Platform, ) -> miette::Result>> { crate::workspace::search::search_exact( - &self.interface, Some(&self.workspace), match_spec, channels, @@ -235,13 +289,7 @@ impl WorkspaceContext { channels: IndexSet, platform: Platform, ) -> miette::Result>> { - crate::workspace::search::search_wildcard( - &self.interface, - Some(&self.workspace), - search, - channels, - platform, - ) - .await + crate::workspace::search::search_wildcard(Some(&self.workspace), search, channels, platform) + .await } } diff --git a/crates/pixi_api/src/lib.rs b/crates/pixi_api/src/lib.rs index e16aac0f22..9b1b6f4ebd 100644 --- a/crates/pixi_api/src/lib.rs +++ b/crates/pixi_api/src/lib.rs @@ -7,6 +7,7 @@ mod interface; pub use interface::Interface; // Reexport for pixi_api consumers +pub use pep508_rs as pep508; pub use pixi_core as core; pub use pixi_manifest as manifest; pub use pixi_pypi_spec as pypi_spec; diff --git a/crates/pixi_api/src/workspace/add/mod.rs b/crates/pixi_api/src/workspace/add/mod.rs new file mode 100644 index 0000000000..22b46a0b2d --- /dev/null +++ b/crates/pixi_api/src/workspace/add/mod.rs @@ -0,0 +1,149 @@ +use indexmap::IndexMap; +use miette::IntoDiagnostic; +use pixi_core::{ + environment::sanity_check_workspace, + workspace::{PypiDeps, UpdateDeps, WorkspaceMut}, +}; +use pixi_manifest::{FeatureName, KnownPreviewFeature, SpecType}; +use pixi_spec::{GitSpec, SourceLocationSpec, SourceSpec}; +use rattler_conda_types::{MatchSpec, PackageName}; + +mod options; + +pub use options::{DependencyOptions, GitOptions}; + +pub async fn add_conda_dep( + mut workspace: WorkspaceMut, + specs: IndexMap, + spec_type: SpecType, + dep_options: DependencyOptions, + git_options: GitOptions, +) -> miette::Result> { + sanity_check_workspace(workspace.workspace()).await?; + + // Add the platform if it is not already present + workspace + .manifest() + .add_platforms(dep_options.platforms.iter(), &FeatureName::DEFAULT)?; + + let mut match_specs = IndexMap::default(); + let mut source_specs = IndexMap::default(); + + // if user passed some git configuration + // we will use it to create pixi source specs + let passed_specs: IndexMap = specs + .into_iter() + .map(|(name, spec)| (name, (spec, spec_type))) + .collect(); + + if let Some(git) = &git_options.git { + if !workspace + .manifest() + .workspace + .preview() + .is_enabled(KnownPreviewFeature::PixiBuild) + { + return Err(miette::miette!( + help = format!( + "Add `preview = [\"pixi-build\"]` to the `workspace` or `project` table of your manifest ({})", + workspace.workspace().workspace.provenance.path.display() + ), + "conda source dependencies are not allowed without enabling the 'pixi-build' preview feature" + )); + } + + source_specs = passed_specs + .iter() + .map(|(name, (_spec, spec_type))| { + let git_spec = GitSpec { + git: git.clone(), + rev: Some(git_options.reference.clone()), + subdirectory: git_options.subdir.clone(), + }; + ( + name.clone(), + ( + SourceSpec { + location: SourceLocationSpec::Git(git_spec), + }, + *spec_type, + ), + ) + }) + .collect(); + } else { + match_specs = passed_specs; + } + + // TODO: add dry_run logic to add + let dry_run = false; + + let update_deps = match Box::pin(workspace.update_dependencies( + match_specs, + IndexMap::default(), + source_specs, + dep_options.no_install, + &dep_options.lock_file_usage, + &dep_options.feature, + &dep_options.platforms, + false, + dry_run, + )) + .await + { + Ok(update_deps) => { + // Write the updated manifest + workspace.save().await.into_diagnostic()?; + update_deps + } + Err(e) => { + workspace.revert().await.into_diagnostic()?; + return Err(e); + } + }; + + Ok(update_deps) +} + +pub async fn add_pypi_dep( + mut workspace: WorkspaceMut, + pypi_deps: PypiDeps, + editable: bool, + options: DependencyOptions, +) -> miette::Result> { + sanity_check_workspace(workspace.workspace()).await?; + + // Add the platform if it is not already present + workspace + .manifest() + .add_platforms(options.platforms.iter(), &FeatureName::DEFAULT)?; + + // TODO: add dry_run logic to add + let dry_run = false; + + let update_deps = match Box::pin(workspace.update_dependencies( + IndexMap::default(), + pypi_deps, + IndexMap::default(), + options.no_install, + &options.lock_file_usage, + &options.feature, + &options.platforms, + editable, + dry_run, + )) + .await + { + Ok(update_deps) => { + // Write the updated manifest + workspace.save().await.into_diagnostic()?; + update_deps + } + Err(e) => { + workspace.revert().await.into_diagnostic()?; + return Err(e); + } + }; + + Ok(update_deps) +} diff --git a/crates/pixi_api/src/workspace/add/options.rs b/crates/pixi_api/src/workspace/add/options.rs new file mode 100644 index 0000000000..332195a6a8 --- /dev/null +++ b/crates/pixi_api/src/workspace/add/options.rs @@ -0,0 +1,24 @@ +use pixi_core::environment::LockFileUsage; +use pixi_manifest::FeatureName; +use pixi_spec::GitReference; +use rattler_conda_types::Platform; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct DependencyOptions { + /// The feature for which the dependency should be modified. + pub feature: FeatureName, + /// The platform for which the dependency should be modified. + pub platforms: Vec, + /// Don't modify the environment, only modify the lock-file. + pub no_install: bool, + pub lock_file_usage: LockFileUsage, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct GitOptions { + pub git: Option, + pub reference: GitReference, + pub subdir: Option, +} diff --git a/crates/pixi_api/src/workspace/mod.rs b/crates/pixi_api/src/workspace/mod.rs index 832de4730d..7991b88a31 100644 --- a/crates/pixi_api/src/workspace/mod.rs +++ b/crates/pixi_api/src/workspace/mod.rs @@ -1,5 +1,10 @@ //! This module contains everything which is related to a Pixi workspace. +pub(crate) mod add; +pub use add::{DependencyOptions, GitOptions}; + +pub(crate) mod remove; + pub(crate) mod init; pub use init::{GitAttributes, InitOptions, ManifestFormat}; diff --git a/crates/pixi_api/src/workspace/remove/mod.rs b/crates/pixi_api/src/workspace/remove/mod.rs new file mode 100644 index 0000000000..59fd8d348a --- /dev/null +++ b/crates/pixi_api/src/workspace/remove/mod.rs @@ -0,0 +1,108 @@ +use indexmap::IndexMap; +use miette::{Context, IntoDiagnostic}; +use pixi_core::{ + InstallFilter, UpdateLockFileOptions, + environment::{LockFileUsage, get_update_lock_file_and_prefix}, + lock_file::{ReinstallPackages, UpdateMode}, + workspace::{PypiDeps, WorkspaceMut}, +}; +use pixi_manifest::{FeaturesExt, SpecType}; +use rattler_conda_types::{MatchSpec, PackageName}; + +use crate::workspace::DependencyOptions; + +pub async fn remove_conda_deps( + mut workspace: WorkspaceMut, + specs: IndexMap, + spec_type: SpecType, + options: DependencyOptions, +) -> miette::Result<()> { + // Prevent removing Python if PyPI dependencies exist + for name in specs.keys() { + if name.as_source() == "python" { + // Check if there are any PyPI dependencies by importing the PypiDependencies trait + let pypi_deps = workspace + .workspace() + .default_environment() + .pypi_dependencies(None); + if !pypi_deps.is_empty() { + let deps_list = pypi_deps + .iter() + .map(|(name, _)| name.as_source()) + .collect::>() + .join(", "); + return Err(miette::miette!( + "Cannot remove Python while PyPI dependencies exist. Please remove these PyPI dependencies first: {}", + deps_list + )); + } + } + } + + for name in specs.keys() { + workspace + .manifest() + .remove_dependency(name, spec_type, &options.platforms, &options.feature) + .wrap_err(format!( + "failed to remove dependency: '{}'", + name.as_source() + ))?; + } + let workspace = workspace.save().await.into_diagnostic()?; + + // TODO: update all environments touched by this feature defined. + // updating prefix after removing from toml + if options.lock_file_usage == LockFileUsage::Update { + get_update_lock_file_and_prefix( + &workspace.default_environment(), + UpdateMode::Revalidate, + UpdateLockFileOptions { + lock_file_usage: options.lock_file_usage, + no_install: options.no_install, + max_concurrent_solves: workspace.config().max_concurrent_solves(), + }, + ReinstallPackages::default(), + &InstallFilter::default(), + ) + .await?; + } + + Ok(()) +} + +pub async fn remove_pypi_deps( + mut workspace: WorkspaceMut, + pypi_deps: PypiDeps, + options: DependencyOptions, +) -> miette::Result<()> { + for name in pypi_deps.keys() { + workspace + .manifest() + .remove_pypi_dependency(name, &options.platforms, &options.feature) + .wrap_err(format!( + "failed to remove PyPI dependency: '{}'", + name.as_source() + ))?; + } + + let workspace = workspace.save().await.into_diagnostic()?; + + // TODO: update all environments touched by this feature defined. + // updating prefix after removing from toml + if options.lock_file_usage == LockFileUsage::Update { + get_update_lock_file_and_prefix( + &workspace.default_environment(), + UpdateMode::Revalidate, + UpdateLockFileOptions { + lock_file_usage: options.lock_file_usage, + no_install: options.no_install, + max_concurrent_solves: workspace.config().max_concurrent_solves(), + }, + ReinstallPackages::default(), + &InstallFilter::default(), + ) + .await?; + } + + Ok(()) +} diff --git a/crates/pixi_api/src/workspace/search/mod.rs b/crates/pixi_api/src/workspace/search/mod.rs index bed4215aff..02b89a7a18 100644 --- a/crates/pixi_api/src/workspace/search/mod.rs +++ b/crates/pixi_api/src/workspace/search/mod.rs @@ -12,10 +12,7 @@ use rattler_repodata_gateway::{GatewayError, RepoData}; use regex::Regex; use strsim::jaro; -use crate::Interface; - -pub async fn search_exact( - _interface: &I, +pub async fn search_exact( workspace: Option<&Workspace>, match_spec: MatchSpec, channels: IndexSet, @@ -103,8 +100,7 @@ pub async fn search_exact( Ok(Some(packages)) } -pub async fn search_wildcard( - _interface: &I, +pub async fn search_wildcard( workspace: Option<&Workspace>, package_name_filter: &str, channels: IndexSet, diff --git a/crates/pixi_api/src/workspace/task/mod.rs b/crates/pixi_api/src/workspace/task/mod.rs index d71509adc5..7470abab33 100644 --- a/crates/pixi_api/src/workspace/task/mod.rs +++ b/crates/pixi_api/src/workspace/task/mod.rs @@ -13,8 +13,7 @@ use rattler_conda_types::Platform; use crate::interface::Interface; -pub async fn list_tasks( - _interface: &I, +pub async fn list_tasks( workspace: &Workspace, environment: Option, ) -> miette::Result>> { diff --git a/crates/pixi_cli/src/add.rs b/crates/pixi_cli/src/add.rs index 60de855f7c..f16c8c11a0 100644 --- a/crates/pixi_cli/src/add.rs +++ b/crates/pixi_cli/src/add.rs @@ -1,14 +1,14 @@ use clap::Parser; -use indexmap::IndexMap; -use miette::IntoDiagnostic; +use pixi_api::{ + WorkspaceContext, + workspace::{DependencyOptions, GitOptions}, +}; use pixi_config::ConfigCli; -use pixi_core::{WorkspaceLocator, environment::sanity_check_workspace, workspace::DependencyType}; -use pixi_manifest::{FeatureName, KnownPreviewFeature, SpecType}; -use pixi_spec::{GitSpec, SourceLocationSpec, SourceSpec}; -use rattler_conda_types::{MatchSpec, PackageName}; +use pixi_core::{DependencyType, WorkspaceLocator}; use crate::{ cli_config::{DependencyConfig, LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig}, + cli_interface::CliInterface, has_specs::HasSpecs, }; @@ -94,132 +94,92 @@ pub struct Args { pub editable: bool, } -pub async fn execute(args: Args) -> miette::Result<()> { - let (dependency_config, no_install_config, lock_file_update_config, workspace_config) = ( - args.dependency_config, - args.no_install_config, - args.lock_file_update_config, - args.workspace_config, - ); +impl TryFrom<&Args> for DependencyOptions { + type Error = miette::Error; + + fn try_from(args: &Args) -> miette::Result { + Ok(DependencyOptions { + feature: args.dependency_config.feature.clone(), + platforms: args.dependency_config.platforms.clone(), + no_install: args.no_install_config.no_install, + lock_file_usage: args.lock_file_update_config.lock_file_usage()?, + }) + } +} + +impl From<&Args> for GitOptions { + fn from(args: &Args) -> Self { + GitOptions { + git: args.dependency_config.git.clone(), + reference: args + .dependency_config + .rev + .clone() + .unwrap_or_default() + .into(), + subdir: args.dependency_config.subdir.clone(), + } + } +} +pub async fn execute(args: Args) -> miette::Result<()> { let workspace = WorkspaceLocator::for_cli() - .with_search_start(workspace_config.workspace_locator_start()) + .with_search_start(args.workspace_config.workspace_locator_start()) .locate()? .with_cli_config(args.config.clone()); - sanity_check_workspace(&workspace).await?; - - let mut workspace = workspace.modify()?; + let workspace_ctx = WorkspaceContext::new(CliInterface {}, workspace.clone()); - // Add the platform if it is not already present - workspace - .manifest() - .add_platforms(dependency_config.platforms.iter(), &FeatureName::DEFAULT)?; - - let (match_specs, source_specs, pypi_deps) = match dependency_config.dependency_type() { + let update_deps = match args.dependency_config.dependency_type() { DependencyType::CondaDependency(spec_type) => { - // if user passed some git configuration - // we will use it to create pixi source specs - let passed_specs: IndexMap = dependency_config - .specs()? - .into_iter() - .map(|(name, spec)| (name, (spec, spec_type))) - .collect(); - - if let Some(git) = &dependency_config.git { - if !workspace - .manifest() - .workspace - .preview() - .is_enabled(KnownPreviewFeature::PixiBuild) - { - return Err(miette::miette!( - help = format!( - "Add `preview = [\"pixi-build\"]` to the `workspace` or `project` table of your manifest ({})", - workspace.workspace().workspace.provenance.path.display() - ), - "conda source dependencies are not allowed without enabling the 'pixi-build' preview feature" - )); - } - - let source_specs = passed_specs - .iter() - .map(|(name, (_spec, spec_type))| { - let git_reference = - dependency_config.rev.clone().unwrap_or_default().into(); - - let git_spec = GitSpec { - git: git.clone(), - rev: Some(git_reference), - subdirectory: dependency_config.subdir.clone(), - }; - ( - name.clone(), - ( - SourceSpec { - location: SourceLocationSpec::Git(git_spec), - }, - *spec_type, - ), - ) - }) - .collect(); - (IndexMap::default(), source_specs, IndexMap::default()) - } else { - (passed_specs, IndexMap::default(), IndexMap::default()) - } + let git_options = GitOptions { + git: args.dependency_config.git.clone(), + reference: args + .dependency_config + .rev + .clone() + .unwrap_or_default() + .into(), + subdir: args.dependency_config.subdir.clone(), + }; + + workspace_ctx + .add_conda_deps( + args.dependency_config.specs()?, + spec_type, + (&args).try_into()?, + git_options, + ) + .await? } DependencyType::PypiDependency => { - let match_specs = IndexMap::default(); - let source_specs = IndexMap::default(); - let pypi_deps = match dependency_config - .vcs_pep508_requirements(workspace.workspace()) + let pypi_deps = match args + .dependency_config + .vcs_pep508_requirements(&workspace) .transpose()? { Some(vcs_reqs) => vcs_reqs .into_iter() .map(|(name, req)| (name, (req, None, None))) .collect(), - None => dependency_config - .pypi_deps(workspace.workspace())? + None => args + .dependency_config + .pypi_deps(&workspace)? .into_iter() .map(|(name, req)| (name, (req, None, None))) .collect(), }; - (match_specs, source_specs, pypi_deps) - } - }; - // TODO: add dry_run logic to add - let dry_run = false; - - let update_deps = match Box::pin(workspace.update_dependencies( - match_specs, - pypi_deps, - source_specs, - no_install_config.no_install, - &lock_file_update_config.lock_file_usage()?, - &dependency_config.feature, - &dependency_config.platforms, - args.editable, - dry_run, - )) - .await - { - Ok(update_deps) => { - // Write the updated manifest - workspace.save().await.into_diagnostic()?; - update_deps - } - Err(e) => { - workspace.revert().await.into_diagnostic()?; - return Err(e); + workspace_ctx + .add_pypi_deps(pypi_deps, args.editable, (&args).try_into()?) + .await? } }; if let Some(update_deps) = update_deps { // Notify the user we succeeded - dependency_config.display_success("Added", update_deps.implicit_constraints); + args.dependency_config + .display_success("Added", update_deps.implicit_constraints); } Ok(()) diff --git a/crates/pixi_cli/src/remove.rs b/crates/pixi_cli/src/remove.rs index af1fb0a1a1..35b01d3407 100644 --- a/crates/pixi_cli/src/remove.rs +++ b/crates/pixi_cli/src/remove.rs @@ -1,15 +1,13 @@ use clap::Parser; -use miette::{Context, IntoDiagnostic}; +use pixi_api::{WorkspaceContext, workspace::DependencyOptions}; use pixi_config::ConfigCli; -use pixi_core::{ - DependencyType, UpdateLockFileOptions, WorkspaceLocator, - environment::{InstallFilter, get_update_lock_file_and_prefix}, - lock_file::{ReinstallPackages, UpdateMode}, -}; -use pixi_manifest::FeaturesExt; +use pixi_core::{DependencyType, WorkspaceLocator}; -use crate::cli_config::{DependencyConfig, NoInstallConfig, WorkspaceConfig}; use crate::{cli_config::LockFileUpdateConfig, has_specs::HasSpecs}; +use crate::{ + cli_config::{DependencyConfig, NoInstallConfig, WorkspaceConfig}, + cli_interface::CliInterface, +}; /// Removes dependencies from the workspace. /// @@ -39,98 +37,52 @@ pub struct Args { pub config: ConfigCli, } -pub async fn execute(args: Args) -> miette::Result<()> { - let (dependency_config, no_install_config, lock_file_update_config, workspace_config) = ( - args.dependency_config, - args.no_install_config, - args.lock_file_update_config, - args.workspace_config, - ); +impl TryFrom<&Args> for DependencyOptions { + type Error = miette::Error; + + fn try_from(args: &Args) -> miette::Result { + Ok(DependencyOptions { + feature: args.dependency_config.feature.clone(), + platforms: args.dependency_config.platforms.clone(), + no_install: args.no_install_config.no_install, + lock_file_usage: args.lock_file_update_config.lock_file_usage()?, + }) + } +} - let mut workspace = WorkspaceLocator::for_cli() - .with_search_start(workspace_config.workspace_locator_start()) +pub async fn execute(args: Args) -> miette::Result<()> { + let workspace = WorkspaceLocator::for_cli() + .with_search_start(args.workspace_config.workspace_locator_start()) .locate()? - .with_cli_config(args.config.clone()) - .modify()?; - let dependency_type = dependency_config.dependency_type(); + .with_cli_config(args.config.clone()); + + let workspace_ctx = WorkspaceContext::new(CliInterface {}, workspace.clone()); - // Prevent removing Python if PyPI dependencies exist - if let DependencyType::CondaDependency(_) = dependency_type { - for name in dependency_config.specs()?.keys() { - if name.as_source() == "python" { - // Check if there are any PyPI dependencies by importing the PypiDependencies trait - let pypi_deps = workspace - .workspace() - .default_environment() - .pypi_dependencies(None); - if !pypi_deps.is_empty() { - let deps_list = pypi_deps - .iter() - .map(|(name, _)| name.as_source()) - .collect::>() - .join(", "); - return Err(miette::miette!( - "Cannot remove Python while PyPI dependencies exist. Please remove these PyPI dependencies first: {}", - deps_list - )); - } - } + match args.dependency_config.dependency_type() { + DependencyType::CondaDependency(spec_type) => { + workspace_ctx + .remove_conda_deps( + args.dependency_config.specs()?, + spec_type, + (&args).try_into()?, + ) + .await?; } - } - match dependency_type { DependencyType::PypiDependency => { - for name in dependency_config.pypi_deps(workspace.workspace())?.keys() { - workspace - .manifest() - .remove_pypi_dependency( - name, - &dependency_config.platforms, - &dependency_config.feature, - ) - .wrap_err(format!( - "failed to remove PyPI dependency: '{}'", - name.as_source() - ))?; - } - } - DependencyType::CondaDependency(spec_type) => { - for name in dependency_config.specs()?.keys() { - workspace - .manifest() - .remove_dependency( - name, - spec_type, - &dependency_config.platforms, - &dependency_config.feature, - ) - .wrap_err(format!( - "failed to remove dependency: '{}'", - name.as_source() - ))?; - } + let pypi_deps = args + .dependency_config + .pypi_deps(&workspace)? + .into_iter() + .map(|(name, req)| (name, (req, None, None))) + .collect(); + workspace_ctx + .remove_pypi_deps(pypi_deps, (&args).try_into()?) + .await?; } }; - let workspace = workspace.save().await.into_diagnostic()?; - - // TODO: update all environments touched by this feature defined. - // updating prefix after removing from toml - if !lock_file_update_config.no_lockfile_update { - get_update_lock_file_and_prefix( - &workspace.default_environment(), - UpdateMode::Revalidate, - UpdateLockFileOptions { - lock_file_usage: lock_file_update_config.lock_file_usage()?, - no_install: no_install_config.no_install, - max_concurrent_solves: workspace.config().max_concurrent_solves(), - }, - ReinstallPackages::default(), - &InstallFilter::default(), - ) - .await?; - } - - dependency_config.display_success("Removed", Default::default()); + args.dependency_config + .display_success("Removed", Default::default()); Ok(()) } diff --git a/crates/pixi_core/src/lock_file/resolve/pypi.rs b/crates/pixi_core/src/lock_file/resolve/pypi.rs index 41cf75ce4b..9217e5e7c5 100644 --- a/crates/pixi_core/src/lock_file/resolve/pypi.rs +++ b/crates/pixi_core/src/lock_file/resolve/pypi.rs @@ -333,14 +333,14 @@ pub async fn resolve_pypi( if !conda_python_packages.is_empty() { tracing::info!( "the following python packages are assumed to be installed by conda: {conda_python_packages}", - conda_python_packages = - conda_python_packages - .values() - .format_with(", ", |(_, p), f| f(&format_args!( - "{name} {version}", - name = &p.name.as_source(), - version = &p.version - ))) + conda_python_packages = conda_python_packages + .values() + .format_with(", ", |(_, p), f| f(&format_args!( + "{name} {version}", + name = &p.name.as_source(), + version = &p.version + ))) + .to_string() ); } else { tracing::info!("there are no python packages installed by conda"); diff --git a/crates/pixi_manifest/src/spec_type.rs b/crates/pixi_manifest/src/spec_type.rs index 115522f1e1..39a7961644 100644 --- a/crates/pixi_manifest/src/spec_type.rs +++ b/crates/pixi_manifest/src/spec_type.rs @@ -1,4 +1,6 @@ -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Copy, Clone, Hash, PartialEq, Eq)] /// What kind of dependency spec do we have pub enum SpecType { /// Host dependencies are used that are needed by the host environment when diff --git a/crates/pixi_pypi_spec/src/name.rs b/crates/pixi_pypi_spec/src/name.rs index 0b2d1fabe4..04fe148deb 100644 --- a/crates/pixi_pypi_spec/src/name.rs +++ b/crates/pixi_pypi_spec/src/name.rs @@ -1,5 +1,5 @@ use pep508_rs::{InvalidNameError, PackageName}; -use serde::{Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::{borrow::Borrow, str::FromStr}; /// A package name for Pypi that also stores the source version of the name. @@ -49,6 +49,16 @@ impl Serialize for PypiPackageName { } } +impl<'de> Deserialize<'de> for PypiPackageName { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } +} + impl PypiPackageName { pub fn from_normalized(normalized: PackageName) -> Self { Self {