diff --git a/docs/cli.md b/docs/cli.md index e81c3ffd8..239a115b6 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -709,9 +709,11 @@ Adds a platform(s) to the project file and updates the lockfile. ##### Options - `--no-install`: do not update the environment, only add changed packages to the lock-file. +- `--feature (-f)`: The feature for which the platform will be added. ```sh pixi project platform add win-64 +pixi project platform add --feature test win-64 ``` ### `project platform list` @@ -737,9 +739,11 @@ Remove platform(s) from the project file and updates the lockfile. ##### Options - `--no-install`: do not update the environment, only add changed packages to the lock-file. +- `--feature (-f)`: The feature for which the platform will be removed. ```sh pixi project platform remove win-64 +pixi project platform remove --feature test win-64 ``` ### `project version get` diff --git a/src/cli/add.rs b/src/cli/add.rs index d5f824701..ced94b1fd 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -2,6 +2,7 @@ use crate::{ consts, environment::{get_up_to_date_prefix, verify_prefix_location_unchanged, LockFileUsage}, project::{manifest::PyPiRequirement, DependencyType, Project, SpecType}, + FeatureName, }; use clap::Parser; use itertools::{Either, Itertools}; @@ -121,7 +122,9 @@ pub async fn execute(args: Args) -> miette::Result<()> { .filter(|p| !project.platforms().contains(p)) .cloned() .collect::>(); - project.manifest.add_platforms(platforms_to_add.iter())?; + project + .manifest + .add_platforms(platforms_to_add.iter(), &FeatureName::Default)?; match dependency_type { DependencyType::CondaDependency(spec_type) => { diff --git a/src/cli/project/platform/add.rs b/src/cli/project/platform/add.rs index 4f18ffba9..b071b9d40 100644 --- a/src/cli/project/platform/add.rs +++ b/src/cli/project/platform/add.rs @@ -1,10 +1,9 @@ use std::str::FromStr; use crate::environment::{get_up_to_date_prefix, LockFileUsage}; -use crate::Project; +use crate::{FeatureName, Project}; use clap::Parser; use indexmap::IndexMap; -use itertools::Itertools; use miette::IntoDiagnostic; use rattler_conda_types::Platform; @@ -17,9 +16,17 @@ pub struct Args { /// Don't update the environment, only add changed packages to the lock-file. #[clap(long)] pub no_install: bool, + + /// The name of the feature to add the platform to. + #[clap(long, short)] + pub feature: Option, } pub async fn execute(mut project: Project, args: Args) -> miette::Result<()> { + let feature_name = args + .feature + .map_or(FeatureName::Default, FeatureName::Named); + // Determine which platforms are missing let platforms = args .platform @@ -28,21 +35,10 @@ pub async fn execute(mut project: Project, args: Args) -> miette::Result<()> { .collect::, _>>() .into_diagnostic()?; - let missing_platforms = platforms - .into_iter() - .filter(|x| !project.platforms().contains(x)) - .collect_vec(); - - if missing_platforms.is_empty() { - eprintln!( - "{}All platform(s) have already been added.", - console::style(console::Emoji("✔ ", "")).green(), - ); - return Ok(()); - } - // Add the platforms to the lock-file - project.manifest.add_platforms(missing_platforms.iter())?; + project + .manifest + .add_platforms(platforms.iter(), &feature_name)?; // Try to update the lock-file with the new channels get_up_to_date_prefix( @@ -55,11 +51,14 @@ pub async fn execute(mut project: Project, args: Args) -> miette::Result<()> { project.save()?; // Report back to the user - for platform in missing_platforms { + for platform in platforms { eprintln!( "{}Added {}", console::style(console::Emoji("✔ ", "")).green(), - platform + match &feature_name { + FeatureName::Default => platform.to_string(), + FeatureName::Named(name) => format!("{} to the feature {}", platform, name), + } ); } diff --git a/src/cli/project/platform/list.rs b/src/cli/project/platform/list.rs index 47272104f..093d1fa2c 100644 --- a/src/cli/project/platform/list.rs +++ b/src/cli/project/platform/list.rs @@ -1,9 +1,21 @@ use crate::Project; pub async fn execute(project: Project) -> miette::Result<()> { - project.platforms().iter().for_each(|platform| { - println!("{}", platform.as_str()); - }); - + project + .environments() + .iter() + .map(|e| { + println!( + "{} {}", + console::style("Environment:").bold().bright(), + e.name().fancy_display() + ); + e.platforms() + }) + .for_each(|c| { + c.into_iter().for_each(|platform| { + println!("- {}", platform.as_str()); + }) + }); Ok(()) } diff --git a/src/cli/project/platform/remove.rs b/src/cli/project/platform/remove.rs index dad5d38f5..f497a5fa2 100644 --- a/src/cli/project/platform/remove.rs +++ b/src/cli/project/platform/remove.rs @@ -1,9 +1,8 @@ use crate::environment::{get_up_to_date_prefix, LockFileUsage}; -use crate::Project; +use crate::{FeatureName, Project}; use clap::Parser; use indexmap::IndexMap; -use itertools::Itertools; use miette::IntoDiagnostic; use rattler_conda_types::Platform; use std::str::FromStr; @@ -17,9 +16,17 @@ pub struct Args { /// Don't update the environment, only remove the platform(s) from the lock-file. #[clap(long)] pub no_install: bool, + + /// The name of the feature to remove the platform from. + #[clap(long, short)] + pub feature: Option, } pub async fn execute(mut project: Project, args: Args) -> miette::Result<()> { + let feature_name = args + .feature + .map_or(FeatureName::Default, FeatureName::Named); + // Determine which platforms to remove let platforms = args .platform @@ -28,23 +35,10 @@ pub async fn execute(mut project: Project, args: Args) -> miette::Result<()> { .collect::, _>>() .into_diagnostic()?; - let platforms_to_remove = platforms - .into_iter() - .filter(|x| project.platforms().contains(x)) - .collect_vec(); - - if platforms_to_remove.is_empty() { - eprintln!( - "{}The platforms(s) are not present.", - console::style(console::Emoji("✔ ", "")).green(), - ); - return Ok(()); - } - // Remove the platform(s) from the manifest project .manifest - .remove_platforms(platforms_to_remove.iter().map(|p| p.to_string()))?; + .remove_platforms(&platforms, &feature_name)?; get_up_to_date_prefix( &project.default_environment(), @@ -56,11 +50,14 @@ pub async fn execute(mut project: Project, args: Args) -> miette::Result<()> { project.save()?; // Report back to the user - for platform in platforms_to_remove { + for platform in platforms { eprintln!( "{}Removed {}", console::style(console::Emoji("✔ ", "")).green(), - platform, + match &feature_name { + FeatureName::Default => platform.to_string(), + FeatureName::Named(name) => format!("{} from the feature {}", platform, name), + }, ); } diff --git a/src/project/manifest/mod.rs b/src/project/manifest/mod.rs index b9bfb74d1..1cb7a9cff 100644 --- a/src/project/manifest/mod.rs +++ b/src/project/manifest/mod.rs @@ -215,55 +215,119 @@ impl Manifest { pub fn add_platforms<'a>( &mut self, platforms: impl Iterator + Clone, + feature_name: &FeatureName, ) -> miette::Result<()> { - // Add to platform table - let platform_array = &mut self.document["project"]["platforms"]; - let platform_array = platform_array - .as_array_mut() - .expect("platforms should be an array"); - - for platform in platforms.clone() { - platform_array.push(platform.to_string()); + let mut stored_platforms = IndexSet::new(); + match feature_name { + FeatureName::Default => { + for platform in platforms { + // TODO: Make platforms a IndexSet to avoid duplicates. + if self + .parsed + .project + .platforms + .value + .iter() + .any(|x| x == platform) + { + continue; + } + self.parsed.project.platforms.value.push(*platform); + + stored_platforms.insert(platform); + } + } + FeatureName::Named(_) => { + for platform in platforms { + match self.parsed.features.entry(feature_name.clone()) { + Entry::Occupied(mut entry) => { + if let Some(platforms) = &mut entry.get_mut().platforms { + if platforms.value.iter().any(|x| x == platform) { + continue; + } + } + // If the feature already exists, just push the new platform + entry + .get_mut() + .platforms + .get_or_insert_with(Default::default) + .value + .push(*platform); + } + Entry::Vacant(entry) => { + // If the feature does not exist, insert a new feature with the new platform + entry.insert(Feature { + name: feature_name.clone(), + platforms: Some(PixiSpanned::from(vec![*platform])), + system_requirements: Default::default(), + targets: Default::default(), + channels: None, + }); + } + } + stored_platforms.insert(platform); + } + } + } + // Then add the platforms to the toml document + let platforms_array = self.specific_array_mut("platforms", feature_name)?; + for platform in stored_platforms { + platforms_array.push(platform.to_string()); } - // Add to manifest - self.parsed.project.platforms.value.extend(platforms); Ok(()) } /// Remove the platform(s) from the project pub fn remove_platforms( &mut self, - platforms: impl IntoIterator>, + platforms: &Vec, + feature_name: &FeatureName, ) -> miette::Result<()> { let mut removed_platforms = Vec::new(); - - for platform in platforms { - // Parse the channel to be removed - let platform_to_remove = Platform::from_str(platform.as_ref()).into_diagnostic()?; - - // Remove the channel if it exists - if let Some(pos) = self - .parsed - .project - .platforms - .value - .iter() - .position(|x| *x == platform_to_remove) - { - self.parsed.project.platforms.value.remove(pos); + match feature_name { + FeatureName::Default => { + for platform in platforms { + if let Some(index) = self + .parsed + .project + .platforms + .value + .iter() + .position(|x| x == platform) + { + self.parsed.project.platforms.value.remove(index); + removed_platforms.push(platform.to_string()); + } + } + } + FeatureName::Named(_) => { + for platform in platforms { + match self.parsed.features.entry(feature_name.clone()) { + Entry::Occupied(mut entry) => { + if let Some(platforms) = &mut entry.get_mut().platforms { + if let Some(index) = + platforms.value.iter().position(|x| x == platform) + { + platforms.value.remove(index); + } + } + } + Entry::Vacant(_entry) => { + return Err(miette!( + "Feature {} does not exist", + feature_name.as_str() + )); + } + } + removed_platforms.push(platform.to_string()); + } } - - removed_platforms.push(platform.as_ref().to_owned()); } - // remove the platforms from the toml - let platform_array = &mut self.document["project"]["platforms"]; - let platform_array = platform_array - .as_array_mut() - .expect("platforms should be an array"); - - platform_array.retain(|x| !removed_platforms.contains(&x.as_str().unwrap().to_string())); + // remove the channels from the toml + let platforms_array = self.specific_array_mut("platforms", feature_name)?; + platforms_array.retain(|x| !removed_platforms.contains(&x.as_str().unwrap().to_string())); Ok(()) } @@ -414,8 +478,12 @@ impl Manifest { .any(|f| f.pypi_dependencies.is_some()) } - /// Returns a mutable reference to the channels array. - fn channels_array_mut(&mut self, feature_name: &FeatureName) -> miette::Result<&mut Array> { + /// Returns a mutable reference to the specified array either in project or feature. + fn specific_array_mut( + &mut self, + array_name: &str, + feature_name: &FeatureName, + ) -> miette::Result<&mut Array> { match feature_name { FeatureName::Default => { let project = &mut self.document["project"]; @@ -423,14 +491,14 @@ impl Manifest { *project = Item::Table(Table::new()); } - let channels = &mut project["channels"]; + let channels = &mut project[array_name]; if channels.is_none() { *channels = Item::Value(Value::Array(Array::new())) } channels .as_array_mut() - .ok_or_else(|| miette::miette!("malformed channels array")) + .ok_or_else(|| miette::miette!("malformed {array_name} array")) } FeatureName::Named(_) => { let feature = &mut self.document["feature"]; @@ -445,14 +513,14 @@ impl Manifest { *feature = Item::Table(Table::new()); } - let channels = &mut feature["channels"]; + let channels = &mut feature[array_name]; if channels.is_none() { *channels = Item::Value(Value::Array(Array::new())) } channels .as_array_mut() - .ok_or_else(|| miette::miette!("malformed channels array")) + .ok_or_else(|| miette::miette!("malformed {array_name} array")) } } } @@ -509,7 +577,7 @@ impl Manifest { } } // Then add the channels to the toml document - let channels_array = self.channels_array_mut(feature_name)?; + let channels_array = self.specific_array_mut("channels", feature_name)?; for channel in stored_channels { channels_array.push(channel); } @@ -566,7 +634,7 @@ impl Manifest { } // remove the channels from the toml - let channels_array = self.channels_array_mut(feature_name)?; + let channels_array = self.specific_array_mut("channels", feature_name)?; channels_array.retain(|x| !removed_channels.contains(&x.as_str().unwrap().to_string())); Ok(()) @@ -1644,12 +1712,50 @@ feature_target_dep = "*" vec![Platform::Linux64, Platform::Win64] ); - manifest.add_platforms([Platform::OsxArm64].iter()).unwrap(); + manifest + .add_platforms([Platform::OsxArm64].iter(), &FeatureName::Default) + .unwrap(); assert_eq!( manifest.parsed.project.platforms.value, vec![Platform::Linux64, Platform::Win64, Platform::OsxArm64] ); + + manifest + .add_platforms( + [Platform::LinuxAarch64, Platform::Osx64].iter(), + &FeatureName::Named("test".to_string()), + ) + .unwrap(); + + assert_eq!( + manifest + .feature(&FeatureName::Named("test".to_string())) + .unwrap() + .platforms + .clone() + .unwrap() + .value, + vec![Platform::LinuxAarch64, Platform::Osx64] + ); + + manifest + .add_platforms( + [Platform::LinuxAarch64, Platform::Win64].iter(), + &FeatureName::Named("test".to_string()), + ) + .unwrap(); + + assert_eq!( + manifest + .feature(&FeatureName::Named("test".to_string())) + .unwrap() + .platforms + .clone() + .unwrap() + .value, + vec![Platform::LinuxAarch64, Platform::Osx64, Platform::Win64] + ); } #[test] @@ -1663,7 +1769,11 @@ feature_target_dep = "*" channels = [] platforms = ["linux-64", "win-64"] - [dependencies] + [feature.test] + platforms = ["linux-64", "win-64", "osx-64"] + + [environments] + test = ["test"] "#; let mut manifest = Manifest::from_str(Path::new(""), file_contents).unwrap(); @@ -1673,12 +1783,43 @@ feature_target_dep = "*" vec![Platform::Linux64, Platform::Win64] ); - manifest.remove_platforms(&vec!["linux-64"]).unwrap(); + manifest + .remove_platforms(&vec![Platform::Linux64], &FeatureName::Default) + .unwrap(); assert_eq!( manifest.parsed.project.platforms.value, vec![Platform::Win64] ); + + assert_eq!( + manifest + .feature(&FeatureName::Named("test".to_string())) + .unwrap() + .platforms + .clone() + .unwrap() + .value, + vec![Platform::Linux64, Platform::Win64, Platform::Osx64] + ); + + manifest + .remove_platforms( + &vec![Platform::Linux64, Platform::Osx64], + &FeatureName::Named("test".to_string()), + ) + .unwrap(); + + assert_eq!( + manifest + .feature(&FeatureName::Named("test".to_string())) + .unwrap() + .platforms + .clone() + .unwrap() + .value, + vec![Platform::Win64] + ); } #[test]