Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add upgrade command to pixi #614

Merged
merged 8 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
142 changes: 87 additions & 55 deletions src/cli/global/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,19 +329,74 @@ 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 platform = Platform::current();
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 @@ -377,8 +432,10 @@ pub async fn execute(args: Args) -> miette::Result<()> {
Transaction::from_current_and_desired(prefix_records, records.iter().cloned(), platform)
.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 +452,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 +480,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
Loading