diff --git a/src/cli/global/common.rs b/src/cli/global/common.rs index 01dc006f9..3e3777ef9 100644 --- a/src/cli/global/common.rs +++ b/src/cli/global/common.rs @@ -3,7 +3,8 @@ use std::path::PathBuf; use indexmap::IndexMap; use miette::IntoDiagnostic; use rattler_conda_types::{ - Channel, ChannelConfig, MatchSpec, PackageName, Platform, PrefixRecord, RepoDataRecord, + Channel, ChannelConfig, MatchSpec, PackageName, ParseStrictness, Platform, PrefixRecord, + RepoDataRecord, }; use rattler_repodata_gateway::sparse::SparseRepoData; use rattler_solve::{resolvo, ChannelPriority, SolverImpl, SolverTask}; @@ -16,6 +17,22 @@ use crate::{ utils::reqwest::build_reqwest_clients, }; +/// A trait to facilitate extraction of packages data from arguments +pub(super) trait HasSpecs { + /// returns packages passed as arguments to the command + fn packages(&self) -> Vec<&str>; + + fn specs(&self) -> miette::Result> { + let mut map = IndexMap::with_capacity(self.packages().len()); + for package in self.packages() { + let spec = MatchSpec::from_str(package, ParseStrictness::Strict).into_diagnostic()?; + let name = package_name(&spec)?; + map.insert(name, spec); + } + + Ok(map) + } +} /// Global binaries directory, default to `$HOME/.pixi/bin` pub struct BinDir(pub PathBuf); @@ -108,7 +125,7 @@ pub fn bin_env_dir() -> Option { /// # Returns /// /// The package name from the given MatchSpec -pub(super) fn package_name(package_matchspec: &MatchSpec) -> miette::Result { +fn package_name(package_matchspec: &MatchSpec) -> miette::Result { package_matchspec.name.clone().ok_or_else(|| { miette::miette!( "could not find package name in MatchSpec {}", diff --git a/src/cli/global/install.rs b/src/cli/global/install.rs index 77019b267..00eb343ce 100644 --- a/src/cli/global/install.rs +++ b/src/cli/global/install.rs @@ -10,9 +10,7 @@ use itertools::Itertools; use miette::IntoDiagnostic; use rattler::install::Transaction; use rattler::package_cache::PackageCache; -use rattler_conda_types::{ - MatchSpec, PackageName, ParseStrictness, Platform, PrefixRecord, RepoDataRecord, -}; +use rattler_conda_types::{PackageName, Platform, PrefixRecord, RepoDataRecord}; use rattler_shell::{ activation::{ActivationVariables, Activator, PathModificationBehavior}, shell::Shell, @@ -22,7 +20,7 @@ use reqwest_middleware::ClientWithMiddleware; use super::common::{ channel_name_from_prefix, find_designated_package, get_client_and_sparse_repodata, - load_package_records, package_name, BinDir, BinEnvDir, + load_package_records, BinDir, BinEnvDir, HasSpecs, }; /// Installs the defined package in a global accessible location. @@ -48,6 +46,12 @@ pub struct Args { config: ConfigCli, } +impl HasSpecs for Args { + fn packages(&self) -> Vec<&str> { + self.package.iter().map(AsRef::as_ref).collect() + } +} + /// Create the environment activation script fn create_activation_script(prefix: &Prefix, shell: ShellEnum) -> miette::Result { let activator = @@ -244,22 +248,13 @@ pub async fn execute(args: Args) -> miette::Result<()> { let config = Config::with_cli_config(&args.config); let channels = config.compute_channels(&args.channel).into_diagnostic()?; - // Find the MatchSpec we want to install - let specs = args - .package - .into_iter() - .map(|package_str| MatchSpec::from_str(&package_str, ParseStrictness::Strict)) - .collect::, _>>() - .into_diagnostic()?; - // Fetch sparse repodata let (authenticated_client, sparse_repodata) = get_client_and_sparse_repodata(&channels, &config).await?; // Install the package(s) let mut executables = vec![]; - for package_matchspec in specs { - let package_name = package_name(&package_matchspec)?; + for (package_name, package_matchspec) in args.specs()? { let records = load_package_records(package_matchspec, &sparse_repodata)?; let (prefix_package, scripts, _) = diff --git a/src/cli/global/list.rs b/src/cli/global/list.rs index 1a6e1684c..f690552b3 100644 --- a/src/cli/global/list.rs +++ b/src/cli/global/list.rs @@ -148,11 +148,9 @@ pub(super) async fn list_global_packages() -> miette::Result> { while let Some(entry) = dir_contents.next_entry().await.into_diagnostic()? { if entry.file_type().await.into_diagnostic()?.is_dir() { - let Ok(name) = PackageName::from_str(entry.file_name().to_string_lossy().as_ref()) - else { - continue; - }; - packages.push(name); + if let Ok(name) = PackageName::from_str(entry.file_name().to_string_lossy().as_ref()) { + packages.push(name); + } } } diff --git a/src/cli/global/remove.rs b/src/cli/global/remove.rs index 122095a4b..c1085b347 100644 --- a/src/cli/global/remove.rs +++ b/src/cli/global/remove.rs @@ -4,12 +4,11 @@ use clap::Parser; use clap_verbosity_flag::{Level, Verbosity}; use itertools::Itertools; use miette::IntoDiagnostic; -use rattler_conda_types::ParseStrictness::Strict; -use rattler_conda_types::{MatchSpec, PackageName}; +use rattler_conda_types::PackageName; use crate::prefix::Prefix; -use super::common::{find_designated_package, BinDir, BinEnvDir}; +use super::common::{find_designated_package, BinDir, BinEnvDir, HasSpecs}; use super::install::{find_and_map_executable_scripts, BinScriptMapping}; /// Removes a package previously installed into a globally accessible location via `pixi global install`. @@ -24,24 +23,14 @@ pub struct Args { verbose: Verbosity, } +impl HasSpecs for Args { + fn packages(&self) -> Vec<&str> { + self.package.iter().map(AsRef::as_ref).collect() + } +} + pub async fn execute(args: Args) -> miette::Result<()> { - // Find the MatchSpec we want to remove - let specs = args - .package - .into_iter() - .map(|package_str| MatchSpec::from_str(&package_str, Strict)) - .collect::, _>>() - .into_diagnostic()?; - let packages = specs - .into_iter() - .map(|spec| { - spec.name - .clone() - .ok_or_else(|| miette::miette!("could not find package name in MatchSpec {}", spec)) - }) - .collect::, _>>()?; - - for package_name in packages { + for (package_name, _) in args.specs()? { remove_global_package(package_name, &args.verbose).await?; } diff --git a/src/cli/global/upgrade.rs b/src/cli/global/upgrade.rs index 9578d350e..d10967b31 100644 --- a/src/cli/global/upgrade.rs +++ b/src/cli/global/upgrade.rs @@ -1,28 +1,28 @@ +use std::collections::HashMap; use std::time::Duration; use clap::Parser; +use indexmap::IndexMap; use indicatif::ProgressBar; use itertools::Itertools; use miette::IntoDiagnostic; -use rattler_conda_types::{Channel, MatchSpec, PackageName, Version}; -use rattler_conda_types::{ParseStrictness, RepoDataRecord}; -use reqwest_middleware::ClientWithMiddleware; +use rattler_conda_types::{Channel, MatchSpec, PackageName}; use crate::config::Config; use crate::progress::{global_multi_progress, long_running_progress_style}; use super::common::{ - find_installed_package, get_client_and_sparse_repodata, load_package_records, package_name, + find_installed_package, get_client_and_sparse_repodata, load_package_records, HasSpecs, }; use super::install::globally_install_package; -use super::list::list_global_packages; /// Upgrade specific package which is installed globally. #[derive(Parser, Debug)] #[clap(arg_required_else_help = true)] pub struct Args { - /// Specifies the package that is to be upgraded. - package: String, + /// Specifies the packages to upgrade. + #[arg(required = true)] + pub specs: Vec, /// Represents the channels from which to upgrade specified package. /// Multiple channels can be specified by using this field multiple times. @@ -37,111 +37,97 @@ pub struct Args { channel: Vec, } -pub async fn execute(args: Args) -> miette::Result<()> { - // Get the MatchSpec we need to upgrade - let package_matchspec = - MatchSpec::from_str(&args.package, ParseStrictness::Strict).into_diagnostic()?; - let package_name = package_name(&package_matchspec)?; - let matchspec_has_version = package_matchspec.version.is_some(); - - // Return with error if this package is not globally installed. - if !list_global_packages() - .await? - .iter() - .any(|global_package| global_package.as_normalized() == package_name.as_normalized()) - { - miette::bail!( - "Package {} is not globally installed", - package_name.as_source() - ); +impl HasSpecs for Args { + fn packages(&self) -> Vec<&str> { + self.specs.iter().map(AsRef::as_ref).collect() } +} - let prefix_record = find_installed_package(&package_name).await?; - let installed_version = prefix_record - .repodata_record - .package_record - .version - .into_version(); - +pub async fn execute(args: Args) -> miette::Result<()> { let config = Config::load_global(); + let specs = args.specs()?; + upgrade_packages(specs, config, &args.channel).await +} - // Figure out what channels we are using - let last_installed_channel = Channel::from_str( - prefix_record.repodata_record.channel.clone(), - config.channel_config(), - ) - .into_diagnostic()?; - - let mut channels = vec![last_installed_channel]; - let input_channels = args - .channel - .iter() - .map(|c| Channel::from_str(c, config.channel_config())) - .collect::, _>>() +pub(super) async fn upgrade_packages( + specs: IndexMap, + config: Config, + cli_channels: &[String], +) -> miette::Result<()> { + // Get channels and versions of globally installed packages + let mut installed_versions = HashMap::with_capacity(specs.len()); + let mut channels = config.compute_channels(cli_channels).into_diagnostic()?; + + for package_name in specs.keys() { + let prefix_record = find_installed_package(package_name).await?; + let last_installed_channel = Channel::from_str( + prefix_record.repodata_record.channel.clone(), + config.channel_config(), + ) .into_diagnostic()?; - channels.extend(input_channels); - // Remove possible duplicates - channels = channels.into_iter().unique().collect::>(); + + channels.push(last_installed_channel); + + let installed_version = prefix_record + .repodata_record + .package_record + .version + .into_version(); + installed_versions.insert(package_name.clone(), installed_version); + } + channels = channels.into_iter().unique().collect(); // Fetch sparse repodata let (authenticated_client, sparse_repodata) = get_client_and_sparse_repodata(&channels, &config).await?; - let records = load_package_records(package_matchspec, &sparse_repodata)?; - let package_record = records - .iter() - .find(|r| r.package_record.name.as_normalized() == package_name.as_normalized()) - .ok_or_else(|| { - miette::miette!( - "Package {} not found in the specified channels", - package_name.as_normalized() - ) - })?; - let toinstall_version = package_record.package_record.version.version().to_owned(); - - if !matchspec_has_version - && toinstall_version.cmp(&installed_version) != std::cmp::Ordering::Greater - { - eprintln!( - "Package {} is already up-to-date", - package_name.as_normalized(), - ); - return Ok(()); + // Upgrade each package when relevant + let mut upgraded = false; + for (package_name, package_matchspec) in specs { + let matchspec_has_version = package_matchspec.version.is_some(); + let records = load_package_records(package_matchspec, &sparse_repodata)?; + let package_record = records + .iter() + .find(|r| r.package_record.name == package_name) + .ok_or_else(|| { + miette::miette!( + "Package {} not found in the specified channels", + package_name.as_normalized() + ) + })?; + let toinstall_version = package_record.package_record.version.version().to_owned(); + let installed_version = installed_versions + .get(&package_name) + .expect("should have the installed version") + .to_owned(); + + // Perform upgrade if a specific version was requested + // OR if a more recent version is available + if matchspec_has_version || toinstall_version > installed_version { + let message = format!( + "{} v{} -> v{}", + package_name.as_normalized(), + installed_version, + toinstall_version + ); + + let pb = global_multi_progress().add(ProgressBar::new_spinner()); + pb.enable_steady_tick(Duration::from_millis(100)); + pb.set_style(long_running_progress_style()); + pb.set_message(format!( + "{} {}", + console::style("Updating").green(), + message + )); + globally_install_package(&package_name, records, authenticated_client.clone()).await?; + pb.finish_with_message(format!("{} {}", console::style("Updated").green(), message)); + upgraded = true; + } } - upgrade_package( - &package_name, - installed_version, - toinstall_version, - records, - authenticated_client, - ) - .await -} + if !upgraded { + eprintln!("Nothing to upgrade"); + } -pub(super) async fn upgrade_package( - package_name: &PackageName, - installed_version: Version, - toinstall_version: Version, - records: Vec, - authenticated_client: ClientWithMiddleware, -) -> miette::Result<()> { - let message = format!( - "{} v{} -> v{}", - package_name.as_normalized(), - installed_version, - toinstall_version - ); - - let pb = global_multi_progress().add(ProgressBar::new_spinner()); - pb.enable_steady_tick(Duration::from_millis(100)); - pb.set_style(long_running_progress_style()); - pb.set_message(format!( - "{} {}", - console::style("Updating").green(), - message - )); - globally_install_package(package_name, records, authenticated_client).await?; - pb.finish_with_message(format!("{} {}", console::style("Updated").green(), message)); Ok(()) } diff --git a/src/cli/global/upgrade_all.rs b/src/cli/global/upgrade_all.rs index 358a480fd..c46ebb0fb 100644 --- a/src/cli/global/upgrade_all.rs +++ b/src/cli/global/upgrade_all.rs @@ -1,17 +1,11 @@ -use std::collections::HashMap; - use clap::Parser; -use itertools::Itertools; -use miette::IntoDiagnostic; -use rattler_conda_types::{Channel, MatchSpec, ParseStrictness}; +use indexmap::IndexMap; + +use rattler_conda_types::MatchSpec; use crate::config::{Config, ConfigCli}; -use super::{ - common::{find_installed_package, get_client_and_sparse_repodata, load_package_records}, - list::list_global_packages, - upgrade::upgrade_package, -}; +use super::{list::list_global_packages, upgrade::upgrade_packages}; /// Upgrade all globally installed packages #[derive(Parser, Debug)] @@ -33,75 +27,19 @@ pub struct Args { } pub async fn execute(args: Args) -> miette::Result<()> { - let packages = list_global_packages().await?; let config = Config::with_cli_config(&args.config); - let mut channels = config.compute_channels(&args.channel).into_diagnostic()?; - - let mut installed_versions = HashMap::with_capacity(packages.len()); - - for package_name in packages.iter() { - let prefix_record = find_installed_package(package_name).await?; - let last_installed_channel = Channel::from_str( - prefix_record.repodata_record.channel.clone(), - config.channel_config(), - ) - .into_diagnostic()?; - - channels.push(last_installed_channel); - - let installed_version = prefix_record - .repodata_record - .package_record - .version - .into_version(); - installed_versions.insert(package_name.as_normalized().to_owned(), installed_version); - } - - // Remove possible duplicates - channels = channels.into_iter().unique().collect::>(); - - // Fetch sparse repodata - let (authenticated_client, sparse_repodata) = - get_client_and_sparse_repodata(&channels, &config).await?; - - let mut upgraded = false; - for package_name in packages.iter() { - let package_matchspec = - MatchSpec::from_str(package_name.as_source(), ParseStrictness::Strict) - .into_diagnostic()?; - let records = load_package_records(package_matchspec, &sparse_repodata)?; - let package_record = records - .iter() - .find(|r| r.package_record.name.as_normalized() == package_name.as_normalized()) - .ok_or_else(|| { - miette::miette!( - "Package {} not found in the specified channels", - package_name.as_normalized() - ) - })?; - let toinstall_version = package_record.package_record.version.version().to_owned(); - let installed_version = installed_versions - .get(package_name.as_normalized()) - .expect("should have the installed version") - .to_owned(); - - // Prvent downgrades - if toinstall_version.cmp(&installed_version) == std::cmp::Ordering::Greater { - upgrade_package( - package_name, - installed_version, - toinstall_version, - records, - authenticated_client.clone(), - ) - .await?; - upgraded = true; - } - } - if !upgraded { - eprintln!("Nothing to upgrade"); + let names = list_global_packages().await?; + let mut specs = IndexMap::with_capacity(names.len()); + for name in names { + specs.insert( + name.clone(), + MatchSpec { + name: Some(name), + ..Default::default() + }, + ); } - Ok(()) + upgrade_packages(specs, config, &args.channel).await }