diff --git a/docs/cli.md b/docs/cli.md index 757833a59..c9d839281 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -326,6 +326,36 @@ Globally installed binary packages: - [bin] zsh-5 ``` +### `global upgrade` + +This command upgrades a globally installed package to the latest version. + +##### Options + +- `--channel (-c)`: specify a channel that the project uses. Defaults to `conda-forge`. (Allowed to be used more than once) + +```shell +pixi global upgrade ruff +pixi global upgrade --channel conda-forge --channel bioconda trackplot +# Or in a more concise form +pixi global upgrade -c conda-forge -c bioconda trackplot +``` + +### `global upgrade-all` + +This command upgrades all globally installed packages to their latest version. + +##### Options + +- `--channel (-c)`: specify a channel that the project uses. Defaults to `conda-forge`. (Allowed to be used more than once) + +```shell +pixi global upgrade-all +pixi global upgrade-all --channel conda-forge --channel bioconda +# Or in a more concise form +pixi global upgrade-all -c conda-forge -c bioconda trackplot +``` + ### `global remove` Removes a package previously installed into a globally accessible location via diff --git a/src/cli/global/install.rs b/src/cli/global/install.rs index e47f0afa4..2f790ef1f 100644 --- a/src/cli/global/install.rs +++ b/src/cli/global/install.rs @@ -329,19 +329,73 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Find the MatchSpec we want to install let package_matchspec = MatchSpec::from_str(&args.package).into_diagnostic()?; - let package_name = package_matchspec.name.clone().ok_or_else(|| { - miette::miette!( - "could not find package name in MatchSpec {}", - package_matchspec - ) - })?; - let platform = Platform::current(); // Fetch sparse repodata - let platform_sparse_repodata = fetch_sparse_repodata(&channels, [platform]).await?; + let platform_sparse_repodata = fetch_sparse_repodata(&channels, [Platform::current()]).await?; + + // Install the package + let (prefix_package, scripts, _) = globally_install_package( + package_matchspec, + &platform_sparse_repodata, + &channel_config, + ) + .await?; + + let channel_name = channel_name_from_prefix(&prefix_package, &channel_config); + let whitespace = console::Emoji(" ", "").to_string(); + + eprintln!( + "{}Installed package {} {} {} from {}", + console::style(console::Emoji("✔ ", "")).green(), + console::style( + prefix_package + .repodata_record + .package_record + .name + .as_source() + ) + .bold(), + console::style(prefix_package.repodata_record.package_record.version).bold(), + console::style(prefix_package.repodata_record.package_record.build).bold(), + channel_name, + ); + + let BinDir(bin_dir) = BinDir::from_existing().await?; + let script_names = scripts + .into_iter() + .map(|path| { + path.strip_prefix(&bin_dir) + .expect("script paths were constructed by joining onto BinDir") + .to_string_lossy() + .to_string() + }) + .join(&format!("\n{whitespace} - ")); + + if is_bin_folder_on_path() { + eprintln!( + "{whitespace}These apps are now globally available:\n{whitespace} - {script_names}", + ) + } else { + let bin_dir = format!("~/{BIN_DIR}"); + eprintln!("{whitespace}These apps have been added to {}\n{whitespace} - {script_names}\n\n{} To use them, make sure to add {} to your PATH", + console::style(&bin_dir).bold(), + console::style("!").yellow().bold(), + console::style(&bin_dir).bold() + ) + } + + Ok(()) +} + +pub(super) async fn globally_install_package( + package_matchspec: MatchSpec, + platform_sparse_repodata: &[SparseRepoData], + channel_config: &ChannelConfig, +) -> miette::Result<(PrefixRecord, Vec, bool)> { + let package_name = package_name(&package_matchspec)?; let available_packages = SparseRepoData::load_records_recursive( - platform_sparse_repodata.iter(), + platform_sparse_repodata, vec![package_name.clone()], None, ) @@ -373,12 +427,17 @@ pub async fn execute(args: Args) -> miette::Result<()> { let prefix_records = prefix.find_installed_packages(None).await?; // Create the transaction that we need - let transaction = - Transaction::from_current_and_desired(prefix_records, records.iter().cloned(), platform) - .into_diagnostic()?; + let transaction = Transaction::from_current_and_desired( + prefix_records, + records.iter().cloned(), + Platform::current(), + ) + .into_diagnostic()?; + + let has_transactions = !transaction.operations.is_empty(); // Execute the transaction if there is work to do - if !transaction.operations.is_empty() { + if has_transactions { // Execute the operations that are returned by the solver. await_in_progress( "creating virtual environment", @@ -395,9 +454,6 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Find the installed package in the environment let prefix_package = find_designated_package(&prefix, &package_name).await?; - let channel = Channel::from_str(&prefix_package.repodata_record.channel, &channel_config) - .map(|ch| friendly_channel_name(&ch)) - .unwrap_or_else(|_| prefix_package.repodata_record.channel.clone()); // Determine the shell to use for the invocation script let shell: ShellEnum = if cfg!(windows) { @@ -426,6 +482,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Check if the bin path is on the path if scripts.is_empty() { + let channel = channel_name_from_prefix(&prefix_package, channel_config); miette::bail!( "could not find an executable entrypoint in package {} {} {} from {}, are you sure it exists?", console::style(prefix_package.repodata_record.package_record.name.as_source()).bold(), @@ -433,50 +490,27 @@ pub async fn execute(args: Args) -> miette::Result<()> { console::style(prefix_package.repodata_record.package_record.build).bold(), channel, ); - } else { - let whitespace = console::Emoji(" ", "").to_string(); - eprintln!( - "{}Installed package {} {} {} from {}", - console::style(console::Emoji("✔ ", "")).green(), - console::style( - prefix_package - .repodata_record - .package_record - .name - .as_source() - ) - .bold(), - console::style(prefix_package.repodata_record.package_record.version).bold(), - console::style(prefix_package.repodata_record.package_record.build).bold(), - channel, - ); - - let BinDir(bin_dir) = BinDir::from_existing().await?; - let script_names = scripts - .into_iter() - .map(|path| { - path.strip_prefix(&bin_dir) - .expect("script paths were constructed by joining onto BinDir") - .to_string_lossy() - .to_string() - }) - .join(&format!("\n{whitespace} - ")); - - if is_bin_folder_on_path() { - eprintln!( - "{whitespace}These apps are now globally available:\n{whitespace} - {script_names}", - ) - } else { - let bin_dir = format!("~/{BIN_DIR}"); - eprintln!("{whitespace}These apps have been added to {}\n{whitespace} - {script_names}\n\n{} To use them, make sure to add {} to your PATH", - console::style(&bin_dir).bold(), - console::style("!").yellow().bold(), - console::style(&bin_dir).bold() - ) - } } - Ok(()) + Ok((prefix_package, scripts, has_transactions)) +} + +fn channel_name_from_prefix( + prefix_package: &PrefixRecord, + channel_config: &ChannelConfig, +) -> String { + Channel::from_str(&prefix_package.repodata_record.channel, channel_config) + .map(|ch| friendly_channel_name(&ch)) + .unwrap_or_else(|_| prefix_package.repodata_record.channel.clone()) +} + +pub(super) fn package_name(package_matchspec: &MatchSpec) -> miette::Result { + package_matchspec.name.clone().ok_or_else(|| { + miette::miette!( + "could not find package name in MatchSpec {}", + package_matchspec + ) + }) } /// Returns the string to add for all arguments passed to the script diff --git a/src/cli/global/list.rs b/src/cli/global/list.rs index 03bb8a3ea..7d03c1582 100644 --- a/src/cli/global/list.rs +++ b/src/cli/global/list.rs @@ -46,19 +46,7 @@ fn print_no_packages_found_message() { } pub async fn execute(_args: Args) -> miette::Result<()> { - let mut packages = vec![]; - let mut dir_contents = tokio::fs::read_dir(bin_env_dir()?) - .await - .into_diagnostic()?; - 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); - } - } + let packages = list_global_packages().await?; let mut package_info = vec![]; @@ -118,3 +106,22 @@ pub async fn execute(_args: Args) -> miette::Result<()> { Ok(()) } + +pub(super) async fn list_global_packages() -> Result, miette::ErrReport> { + let mut packages = vec![]; + let mut dir_contents = tokio::fs::read_dir(bin_env_dir()?) + .await + .into_diagnostic()?; + + 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); + } + } + + Ok(packages) +} diff --git a/src/cli/global/mod.rs b/src/cli/global/mod.rs index c71e07a1e..0fe8c4249 100644 --- a/src/cli/global/mod.rs +++ b/src/cli/global/mod.rs @@ -2,6 +2,8 @@ use clap::Parser; mod install; mod list; mod remove; +mod upgrade; +mod upgrade_all; #[derive(Debug, Parser)] pub enum Command { @@ -11,6 +13,10 @@ pub enum Command { Remove(remove::Args), #[clap(alias = "ls")] List(list::Args), + #[clap(alias = "u")] + Upgrade(upgrade::Args), + #[clap(alias = "ua")] + UpgradeAll(upgrade_all::Args), } /// Global is the main entry point for the part of pixi that executes on the global(system) level. @@ -27,6 +33,8 @@ pub async fn execute(cmd: Args) -> miette::Result<()> { Command::Install(args) => install::execute(args).await?, Command::Remove(args) => remove::execute(args).await?, Command::List(args) => list::execute(args).await?, + Command::Upgrade(args) => upgrade::execute(args).await?, + Command::UpgradeAll(args) => upgrade_all::execute(args).await?, }; Ok(()) } diff --git a/src/cli/global/upgrade.rs b/src/cli/global/upgrade.rs new file mode 100644 index 000000000..bab712d79 --- /dev/null +++ b/src/cli/global/upgrade.rs @@ -0,0 +1,82 @@ +use std::str::FromStr; + +use clap::Parser; +use miette::IntoDiagnostic; +use rattler_conda_types::{Channel, ChannelConfig, MatchSpec, Platform}; + +use crate::repodata::fetch_sparse_repodata; + +use super::{install::globally_install_package, 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, + + /// Represents the channels from which to upgrade specified package. + /// Multiple channels can be specified by using this field multiple times. + /// + /// When specifying a channel, it is common that the selected channel also + /// depends on the `conda-forge` channel. + /// For example: `pixi global upgrade --channel conda-forge --channel bioconda`. + /// + /// By default, if no channel is provided, `conda-forge` is used. + #[clap(short, long, default_values = ["conda-forge"])] + channel: Vec, +} + +pub async fn execute(args: Args) -> miette::Result<()> { + let package = args.package; + // Figure out what channels we are using + let channel_config = ChannelConfig::default(); + let channels = args + .channel + .iter() + .map(|c| Channel::from_str(c, &channel_config)) + .collect::, _>>() + .into_diagnostic()?; + + // Find the MatchSpec we want to install + let package_matchspec = MatchSpec::from_str(&package).into_diagnostic()?; + + // Return with error if this package is not globally installed. + if !list_global_packages() + .await? + .iter() + .any(|global_package| global_package.as_source() == package) + { + miette::bail!( + "{} package is not globally installed", + console::style("!").yellow().bold() + ); + } + + // Fetch sparse repodata + let platform_sparse_repodata = fetch_sparse_repodata(&channels, [Platform::current()]).await?; + + // Install the package + let (package_record, _, upgraded) = globally_install_package( + package_matchspec, + &platform_sparse_repodata, + &channel_config, + ) + .await?; + + let package_record = package_record.repodata_record.package_record; + if upgraded { + eprintln!( + "Updated package {} to version {}", + package_record.name.as_normalized(), + package_record.version + ); + } else { + eprintln!( + "Package {} is already up-to-date", + package_record.name.as_normalized(), + ); + } + + Ok(()) +} diff --git a/src/cli/global/upgrade_all.rs b/src/cli/global/upgrade_all.rs new file mode 100644 index 000000000..4606bddb1 --- /dev/null +++ b/src/cli/global/upgrade_all.rs @@ -0,0 +1,74 @@ +use clap::Parser; +use futures::{stream, StreamExt, TryStreamExt}; +use miette::IntoDiagnostic; +use rattler_conda_types::{Channel, ChannelConfig, Platform}; + +use crate::repodata::fetch_sparse_repodata; + +use super::{install::globally_install_package, list::list_global_packages}; + +const UPGRADE_ALL_CONCURRENCY: usize = 5; + +/// Upgrade all globally installed packages +#[derive(Parser, Debug)] +pub struct Args { + /// Represents the channels from which to upgrade packages. + /// Multiple channels can be specified by using this field multiple times. + /// + /// When specifying a channel, it is common that the selected channel also + /// depends on the `conda-forge` channel. + /// For example: `pixi global upgrade-all --channel conda-forge --channel bioconda`. + /// + /// By default, if no channel is provided, `conda-forge` is used. + #[clap(short, long, default_values = ["conda-forge"])] + channel: Vec, +} + +pub async fn execute(args: Args) -> miette::Result<()> { + // Figure out what channels we are using + let channel_config = ChannelConfig::default(); + let channels = args + .channel + .iter() + .map(|c| Channel::from_str(c, &channel_config)) + .collect::, _>>() + .into_diagnostic()?; + + let packages = list_global_packages().await?; + + // Fetch sparse repodata + let platform_sparse_repodata = fetch_sparse_repodata(&channels, [Platform::current()]).await?; + + let tasks = packages + .iter() + .map(|package| package.as_source().parse()) + .collect::, _>>() + .into_diagnostic()?; + + let task_stream = stream::iter(tasks) + .map(|matchspec| { + globally_install_package(matchspec, &platform_sparse_repodata, &channel_config) + }) + .buffered(UPGRADE_ALL_CONCURRENCY); + + let res: Vec<_> = task_stream.try_collect().await?; + + let mut packages_upgraded = 0; + for (prefix_record, _, upgraded) in res { + if upgraded { + packages_upgraded += 1; + let record = prefix_record.repodata_record.package_record; + eprintln!( + "Upgraded {} {}", + console::style(record.name.as_normalized()).bold(), + console::style(record.version).bold(), + ); + } + } + + if packages_upgraded == 0 { + eprintln!("Nothing to upgrade"); + } + + Ok(()) +}