Skip to content

Commit

Permalink
feat: add upgrade command to pixi (#614)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueleo authored Jan 8, 2024
1 parent 89356a7 commit e70fd4a
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 71 deletions.
30 changes: 30 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
150 changes: 92 additions & 58 deletions src/cli/global/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>, 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,
)
Expand Down Expand Up @@ -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",
Expand All @@ -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) {
Expand Down Expand Up @@ -426,57 +482,35 @@ 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(),
console::style(prefix_package.repodata_record.package_record.version).bold(),
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<PackageName> {
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
Expand Down
33 changes: 20 additions & 13 deletions src/cli/global/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![];

Expand Down Expand Up @@ -118,3 +106,22 @@ pub async fn execute(_args: Args) -> miette::Result<()> {

Ok(())
}

pub(super) async fn list_global_packages() -> Result<Vec<PackageName>, 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)
}
8 changes: 8 additions & 0 deletions src/cli/global/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use clap::Parser;
mod install;
mod list;
mod remove;
mod upgrade;
mod upgrade_all;

#[derive(Debug, Parser)]
pub enum Command {
Expand All @@ -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.
Expand All @@ -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(())
}
82 changes: 82 additions & 0 deletions src/cli/global/upgrade.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

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::<Result<Vec<Channel>, _>>()
.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(())
}
Loading

0 comments on commit e70fd4a

Please sign in to comment.