diff --git a/Cargo.lock b/Cargo.lock index 681d0d6117..bc807af7bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5154,9 +5154,9 @@ dependencies = [ [[package]] name = "rattler" -version = "0.33.0" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca1e7955660f5717aa727376dbb031ee0d541ed545d1006adbb49765ba8fd986" +checksum = "454e5091f529a7c24f0b568b7c00b3c978ffb9c4020db8bb1e10dd6e73c9aaec" dependencies = [ "anyhow", "clap", @@ -5198,9 +5198,9 @@ dependencies = [ [[package]] name = "rattler_cache" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f5e8a6816a944c9f213aced8b5c705afbe206ae2ee6aeb5833ea499a484b629" +checksum = "8f84f8ac550603c11d8c681e83c441c0565a29907c40583114b78fc3e31f72bb" dependencies = [ "anyhow", "dashmap", @@ -5230,9 +5230,9 @@ dependencies = [ [[package]] name = "rattler_conda_types" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363f737d32024159a5ef4924a688a4cb0aa0d12a483ef04461cdbbd6669978bd" +checksum = "0ea41e41b171d7185f57c4fa0e19ef145c96604ba151479ec070f928325dbe1a" dependencies = [ "chrono", "dirs 6.0.0", @@ -5286,9 +5286,9 @@ dependencies = [ [[package]] name = "rattler_lock" -version = "0.22.47" +version = "0.22.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7705311b8999eaa43831113fff16cf209b61727894312723d5d43bd07de0f79a" +checksum = "d53b8a7a04cefca5f0ed5463c72bdcb232d7ae3eb27f979569bc6e90eb391763" dependencies = [ "chrono", "file_url", @@ -5321,9 +5321,9 @@ dependencies = [ [[package]] name = "rattler_menuinst" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c37a0dc31ac7873df2fae69ed680b40152ec6eeeb71e01b14014073eb64cb18f" +checksum = "c82434c783daf87bdcfee820db93fe12119cf469ca5abc174c8c45963d48aff2" dependencies = [ "chrono", "configparser", @@ -5351,9 +5351,9 @@ dependencies = [ [[package]] name = "rattler_networking" -version = "0.22.9" +version = "0.22.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2825d73f0c93abd84083e63cdbc892565b30effbb573e4fed6fa340ca3525f51" +checksum = "6bb7aa16bcfa0f53ddbf580c58e43bd8ad7fd8a643c767c365ebf033734d4cd1" dependencies = [ "anyhow", "async-trait", @@ -5382,9 +5382,9 @@ dependencies = [ [[package]] name = "rattler_package_streaming" -version = "0.22.33" +version = "0.22.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ad4e41b0d7cff5d4aad9f608f88ed0cc28c2a3dfcff334c17bc63f30da0643" +checksum = "d55a377f9b969915695c7aa994740ca157221f6a63d03812404e8d393d81e517" dependencies = [ "bzip2", "chrono", @@ -5423,9 +5423,9 @@ dependencies = [ [[package]] name = "rattler_repodata_gateway" -version = "0.22.1" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e99b0495d5cc946c808ab417ddc735cd9125020d66b3c12bc2a898a0b985f3" +checksum = "e6073881466bbd0e726061451e20341df1c4331220d5b489b09549388856d5da" dependencies = [ "anyhow", "async-compression", @@ -5480,9 +5480,9 @@ dependencies = [ [[package]] name = "rattler_shell" -version = "0.22.23" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afdea22518ce07cd94dda80edb82ea1ebc6cc3388d21b6933de1bd2dc3ca530" +checksum = "7c25e56d8c1b003bae96de82c498d550ddea9677825ad52a4d1ececf3ef33613" dependencies = [ "enum_dispatch", "fs-err", @@ -5499,9 +5499,9 @@ dependencies = [ [[package]] name = "rattler_solve" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c656c89048348a6675f29f886cfc001cf4af1bae156b18dd9c39fcea5dfa4687" +checksum = "fc403ea44cf8a4c8f382992ce172fb184b1bd99703bf981c46a4acd6ea010dae" dependencies = [ "chrono", "futures", @@ -5519,9 +5519,9 @@ dependencies = [ [[package]] name = "rattler_virtual_packages" -version = "2.0.6" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf18e5372605eb4b4548108af4fd1ca39906d6af1498f8e1db4ffd734b303fb9" +checksum = "ef443a173c98db5ed56c8de62caad2f2f9569a2945cb520c2e73ab800b029708" dependencies = [ "archspec", "libloading", diff --git a/Cargo.toml b/Cargo.toml index 92b55f9f43..d9e7a20b3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,21 +114,21 @@ which = "7.0.2" # Rattler crates file_url = "0.2.3" -rattler = { version = "0.33.0", default-features = false } -rattler_cache = { version = "0.3.14", default-features = false } -rattler_conda_types = { version = "0.31.4", default-features = false, features = [ +rattler = { version = "0.33.1", default-features = false } +rattler_cache = { version = "0.3.15", default-features = false } +rattler_conda_types = { version = "0.31.5", default-features = false, features = [ "rayon", ] } rattler_digest = { version = "1.0.8", default-features = false } -rattler_lock = { version = "0.22.47", default-features = false } -rattler_menuinst = { version = "0.2.3", default-features = false } -rattler_networking = { version = "0.22.9", default-features = false, features = [ +rattler_lock = { version = "0.22.48", default-features = false } +rattler_menuinst = { version = "0.2.4", default-features = false } +rattler_networking = { version = "0.22.10", default-features = false, features = [ "google-cloud-auth", ] } -rattler_repodata_gateway = { version = "0.22.1", default-features = false } -rattler_shell = { version = "0.22.23", default-features = false } -rattler_solve = { version = "1.4.1", default-features = false } -rattler_virtual_packages = { version = "2.0.6", default-features = false } +rattler_repodata_gateway = { version = "0.22.2", default-features = false } +rattler_shell = { version = "0.22.24", default-features = false } +rattler_solve = { version = "1.4.2", default-features = false } +rattler_virtual_packages = { version = "2.0.8", default-features = false } # Bumping this to a higher version breaks the Windows path handling. url = "2.5.4" diff --git a/docs/reference/cli/pixi/global.md b/docs/reference/cli/pixi/global.md index 79a7b9125e..d1640b98f9 100644 --- a/docs/reference/cli/pixi/global.md +++ b/docs/reference/cli/pixi/global.md @@ -22,6 +22,7 @@ pixi global | [`list`](global/list.md) | Lists all packages previously installed into a globally accessible location via `pixi global install`. | | [`sync`](global/sync.md) | Sync global manifest with installed environments | | [`expose`](global/expose.md) | Interact with the exposure of binaries in the global environment | +| [`shortcut`](global/shortcut.md) | Interact with the shortcuts on your machine | | [`update`](global/update.md) | Updates environments in the global environment | diff --git a/docs/reference/cli/pixi/global/install.md b/docs/reference/cli/pixi/global/install.md index 261b347408..bda84a4853 100644 --- a/docs/reference/cli/pixi/global/install.md +++ b/docs/reference/cli/pixi/global/install.md @@ -33,7 +33,7 @@ pixi global install [OPTIONS] ...
May be provided more than once. - `--force-reinstall` : Specifies that the environment should be reinstalled -- `--no-shortcut` +- `--no-shortcuts` : Specifies that no shortcuts should be created for the installed packages ## Config Options diff --git a/docs/reference/cli/pixi/global/shortcut.md b/docs/reference/cli/pixi/global/shortcut.md new file mode 100644 index 0000000000..9820d91b4e --- /dev/null +++ b/docs/reference/cli/pixi/global/shortcut.md @@ -0,0 +1,21 @@ + +# [pixi](../../pixi.md) [global](../global.md) shortcut + +## About +Interact with the shortcuts on your machine + +--8<-- "docs/reference/cli/pixi/global/shortcut_extender.md:description" + +## Usage +``` +pixi global shortcut +``` + +## Subcommands +| Command | Description | +|---------|-------------| +| [`add`](shortcut/add.md) | Add a shortcut from an environment to your machine. | +| [`remove`](shortcut/remove.md) | Remove shortcuts from your machine | + + +--8<-- "docs/reference/cli/pixi/global/shortcut_extender.md:example" diff --git a/docs/reference/cli/pixi/global/shortcut/add.md b/docs/reference/cli/pixi/global/shortcut/add.md new file mode 100644 index 0000000000..feaeea256f --- /dev/null +++ b/docs/reference/cli/pixi/global/shortcut/add.md @@ -0,0 +1,37 @@ + +# [pixi](../../../pixi.md) [global](../../global.md) [shortcut](../shortcut.md) add + +## About +Add a shortcut from an environment to your machine. + +--8<-- "docs/reference/cli/pixi/global/shortcut/add_extender.md:description" + +## Usage +``` +pixi global shortcut add [OPTIONS] --environment [PACKAGE]... +``` + +## Arguments +- `` +: The package name to add the shortcuts from +
May be provided more than once. + +## Options +- `--environment (-e) ` +: The environment from which the shortcut should be added +
**required**: `true` + +## Config Options +- `--tls-no-verify` +: Do not verify the TLS certificate of the server +- `--auth-file ` +: Path to the file containing the authentication token +- `--pypi-keyring-provider ` +: Specifies whether to use the keyring to look up credentials for PyPI +
**options**: `disabled`, `subprocess` +- `--concurrent-solves ` +: Max concurrent solves, default is the number of CPUs +- `--concurrent-downloads ` +: Max concurrent network requests, default is `50` + +--8<-- "docs/reference/cli/pixi/global/shortcut/add_extender.md:example" diff --git a/docs/reference/cli/pixi/global/shortcut/remove.md b/docs/reference/cli/pixi/global/shortcut/remove.md new file mode 100644 index 0000000000..5e0c6d44f1 --- /dev/null +++ b/docs/reference/cli/pixi/global/shortcut/remove.md @@ -0,0 +1,32 @@ + +# [pixi](../../../pixi.md) [global](../../global.md) [shortcut](../shortcut.md) remove + +## About +Remove shortcuts from your machine + +--8<-- "docs/reference/cli/pixi/global/shortcut/remove_extender.md:description" + +## Usage +``` +pixi global shortcut remove [OPTIONS] [SHORTCUT]... +``` + +## Arguments +- `` +: The shortcut that should be removed +
May be provided more than once. + +## Config Options +- `--tls-no-verify` +: Do not verify the TLS certificate of the server +- `--auth-file ` +: Path to the file containing the authentication token +- `--pypi-keyring-provider ` +: Specifies whether to use the keyring to look up credentials for PyPI +
**options**: `disabled`, `subprocess` +- `--concurrent-solves ` +: Max concurrent solves, default is the number of CPUs +- `--concurrent-downloads ` +: Max concurrent network requests, default is `50` + +--8<-- "docs/reference/cli/pixi/global/shortcut/remove_extender.md:example" diff --git a/pixi_docs/Cargo.lock b/pixi_docs/Cargo.lock index eb03d1fa75..657a8d926f 100644 --- a/pixi_docs/Cargo.lock +++ b/pixi_docs/Cargo.lock @@ -4983,9 +4983,9 @@ dependencies = [ [[package]] name = "rattler" -version = "0.33.0" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca1e7955660f5717aa727376dbb031ee0d541ed545d1006adbb49765ba8fd986" +checksum = "454e5091f529a7c24f0b568b7c00b3c978ffb9c4020db8bb1e10dd6e73c9aaec" dependencies = [ "anyhow", "clap", @@ -5027,9 +5027,9 @@ dependencies = [ [[package]] name = "rattler_cache" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f5e8a6816a944c9f213aced8b5c705afbe206ae2ee6aeb5833ea499a484b629" +checksum = "8f84f8ac550603c11d8c681e83c441c0565a29907c40583114b78fc3e31f72bb" dependencies = [ "anyhow", "dashmap", @@ -5059,9 +5059,9 @@ dependencies = [ [[package]] name = "rattler_conda_types" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363f737d32024159a5ef4924a688a4cb0aa0d12a483ef04461cdbbd6669978bd" +checksum = "0ea41e41b171d7185f57c4fa0e19ef145c96604ba151479ec070f928325dbe1a" dependencies = [ "chrono", "dirs 6.0.0", @@ -5115,9 +5115,9 @@ dependencies = [ [[package]] name = "rattler_lock" -version = "0.22.47" +version = "0.22.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7705311b8999eaa43831113fff16cf209b61727894312723d5d43bd07de0f79a" +checksum = "d53b8a7a04cefca5f0ed5463c72bdcb232d7ae3eb27f979569bc6e90eb391763" dependencies = [ "chrono", "file_url", @@ -5150,9 +5150,9 @@ dependencies = [ [[package]] name = "rattler_menuinst" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c37a0dc31ac7873df2fae69ed680b40152ec6eeeb71e01b14014073eb64cb18f" +checksum = "c82434c783daf87bdcfee820db93fe12119cf469ca5abc174c8c45963d48aff2" dependencies = [ "chrono", "configparser", @@ -5180,9 +5180,9 @@ dependencies = [ [[package]] name = "rattler_networking" -version = "0.22.9" +version = "0.22.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2825d73f0c93abd84083e63cdbc892565b30effbb573e4fed6fa340ca3525f51" +checksum = "6bb7aa16bcfa0f53ddbf580c58e43bd8ad7fd8a643c767c365ebf033734d4cd1" dependencies = [ "anyhow", "async-trait", @@ -5211,9 +5211,9 @@ dependencies = [ [[package]] name = "rattler_package_streaming" -version = "0.22.33" +version = "0.22.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ad4e41b0d7cff5d4aad9f608f88ed0cc28c2a3dfcff334c17bc63f30da0643" +checksum = "d55a377f9b969915695c7aa994740ca157221f6a63d03812404e8d393d81e517" dependencies = [ "bzip2", "chrono", @@ -5252,9 +5252,9 @@ dependencies = [ [[package]] name = "rattler_repodata_gateway" -version = "0.22.1" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e99b0495d5cc946c808ab417ddc735cd9125020d66b3c12bc2a898a0b985f3" +checksum = "e6073881466bbd0e726061451e20341df1c4331220d5b489b09549388856d5da" dependencies = [ "anyhow", "async-compression", @@ -5309,9 +5309,9 @@ dependencies = [ [[package]] name = "rattler_shell" -version = "0.22.23" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afdea22518ce07cd94dda80edb82ea1ebc6cc3388d21b6933de1bd2dc3ca530" +checksum = "7c25e56d8c1b003bae96de82c498d550ddea9677825ad52a4d1ececf3ef33613" dependencies = [ "enum_dispatch", "fs-err", @@ -5328,9 +5328,9 @@ dependencies = [ [[package]] name = "rattler_solve" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c656c89048348a6675f29f886cfc001cf4af1bae156b18dd9c39fcea5dfa4687" +checksum = "fc403ea44cf8a4c8f382992ce172fb184b1bd99703bf981c46a4acd6ea010dae" dependencies = [ "chrono", "futures", @@ -5348,9 +5348,9 @@ dependencies = [ [[package]] name = "rattler_virtual_packages" -version = "2.0.6" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf18e5372605eb4b4548108af4fd1ca39906d6af1498f8e1db4ffd734b303fb9" +checksum = "ef443a173c98db5ed56c8de62caad2f2f9569a2945cb520c2e73ab800b029708" dependencies = [ "archspec", "libloading", diff --git a/src/cli/global/install.rs b/src/cli/global/install.rs index e63daec878..18e7bf5be9 100644 --- a/src/cli/global/install.rs +++ b/src/cli/global/install.rs @@ -74,8 +74,8 @@ pub struct Args { force_reinstall: bool, /// Specifies that no shortcuts should be created for the installed packages. - #[arg(action, long)] - no_shortcut: bool, + #[arg(action, long, alias = "no-shortcut")] + no_shortcuts: bool, } impl HasSpecs for Args { @@ -226,7 +226,7 @@ async fn setup_environment( sync_exposed_names(env_name, project, args).await?; // Add shortcuts - if !args.no_shortcut { + if !args.no_shortcuts { let prefix = project.environment_prefix(env_name).await?; for (package_name, _) in specs.iter() { let prefix_record = prefix.find_designated_package(package_name).await?; diff --git a/src/cli/global/mod.rs b/src/cli/global/mod.rs index fc155b6228..593c929999 100644 --- a/src/cli/global/mod.rs +++ b/src/cli/global/mod.rs @@ -8,6 +8,7 @@ mod expose; mod install; mod list; mod remove; +mod shortcut; mod sync; mod uninstall; mod update; @@ -31,6 +32,8 @@ pub enum Command { #[clap(visible_alias = "e")] #[command(subcommand)] Expose(expose::SubCommand), + #[command(subcommand)] + Shortcut(shortcut::SubCommand), Update(update::Args), #[command(hide = true)] Upgrade(upgrade::Args), @@ -59,6 +62,7 @@ pub async fn execute(cmd: Args) -> miette::Result<()> { Command::List(args) => list::execute(args).await?, Command::Sync(args) => sync::execute(args).await?, Command::Expose(subcommand) => expose::execute(subcommand).await?, + Command::Shortcut(subcommand) => shortcut::execute(subcommand).await?, Command::Update(args) => update::execute(args).await?, Command::Upgrade(args) => upgrade::execute(args).await?, Command::UpgradeAll(args) => upgrade_all::execute(args).await?, diff --git a/src/cli/global/shortcut.rs b/src/cli/global/shortcut.rs new file mode 100644 index 0000000000..a51ce84750 --- /dev/null +++ b/src/cli/global/shortcut.rs @@ -0,0 +1,185 @@ +use crate::global::Project; +use crate::{ + cli::global::revert_environment_after_error, + global::{EnvironmentName, StateChanges}, +}; +use clap::Parser; +use fancy_display::FancyDisplay; +use miette::Context; +use pixi_config::{Config, ConfigCli}; +use rattler_conda_types::PackageName; +use std::collections::HashMap; + +/// Add a shortcut from an environment to your machine. +#[derive(Parser, Debug)] +#[clap(arg_required_else_help = true, verbatim_doc_comment)] +pub struct AddArgs { + /// The package name to add the shortcuts from. + #[arg(num_args = 1.., value_name = "PACKAGE")] + packages: Vec, + + /// The environment from which the shortcut should be added. + #[clap(short, long)] + environment: EnvironmentName, + + #[clap(flatten)] + config: ConfigCli, +} + +/// Remove shortcuts from your machine. +#[derive(Parser, Debug)] +pub struct RemoveArgs { + /// The shortcut that should be removed. + #[arg(num_args = 1.., value_name = "SHORTCUT")] + shortcuts: Vec, + + #[clap(flatten)] + config: ConfigCli, +} + +/// Interact with the shortcuts on your machine. +#[derive(Parser, Debug)] +#[clap(group(clap::ArgGroup::new("command")))] +pub enum SubCommand { + #[clap(name = "add")] + Add(AddArgs), + #[clap(name = "remove")] + Remove(RemoveArgs), +} + +/// Add or remove shortcuts from your machine +pub async fn execute(args: SubCommand) -> miette::Result<()> { + match args { + SubCommand::Add(args) => add(args).await?, + SubCommand::Remove(args) => remove(args).await?, + } + Ok(()) +} + +pub async fn add(args: AddArgs) -> miette::Result<()> { + let config = Config::with_cli_config(&args.config); + let project_original = Project::discover_or_create() + .await? + .with_cli_config(config.clone()); + + async fn apply_changes( + args: &AddArgs, + project: &mut Project, + ) -> Result { + let env_name = &args.environment; + let mut state_changes = StateChanges::new_with_env(env_name.clone()); + for name in &args.packages { + project.manifest.add_shortcut(env_name, name)?; + } + state_changes |= project.sync_environment(env_name, None).await?; + Ok(state_changes) + } + + let mut project_modified = project_original.clone(); + match apply_changes(&args, &mut project_modified).await { + Ok(state_changes) => { + project_modified.manifest.save().await?; + state_changes.report(); + Ok(()) + } + Err(err) => { + if let Err(revert_err) = + revert_environment_after_error(&args.environment, &project_original).await + { + tracing::warn!("Reverting of the operation failed"); + tracing::info!("Reversion error: {:?}", revert_err); + } + Err(err) + } + } +} + +pub async fn remove(args: RemoveArgs) -> miette::Result<()> { + let config = Config::with_cli_config(&args.config); + let project_original = Project::discover_or_create() + .await? + .with_cli_config(config.clone()); + + async fn apply_changes( + shortcuts: Vec, + env_name: &EnvironmentName, + project: &mut Project, + ) -> Result { + let mut state_changes = StateChanges::new_with_env(env_name.clone()); + + for shortcut in shortcuts { + project + .manifest + .remove_shortcut(&shortcut, env_name) + .wrap_err_with(|| { + format!( + "Couldn't remove shortcut name '{}' from {} environment", + shortcut.as_normalized(), + env_name.fancy_display() + ) + })?; + } + + state_changes |= project.sync_environment(env_name, None).await?; + project.manifest.save().await?; + Ok(state_changes) + } + + let to_remove_shortcuts_map: HashMap> = project_original + .environments() + .iter() + .filter_map(|(env_name, env)| { + env.shortcuts.as_ref().map(|shortcuts| { + let to_remove = shortcuts + .iter() + .filter(|shortcut| args.shortcuts.contains(shortcut)) + .cloned() + .collect::>(); + (!to_remove.is_empty()).then(|| (env_name.clone(), to_remove)) + })? + }) + .collect(); + + if to_remove_shortcuts_map.is_empty() { + miette::bail!( + "No shortcuts found with name(s): {}", + console::style( + args.shortcuts + .iter() + .map(|s| s.as_normalized()) + .collect::>() + .join(", ") + ) + .bold() + .yellow() + ); + } + + let mut last_updated_project = project_original; + for (env_name, shortcuts) in to_remove_shortcuts_map { + let mut project = last_updated_project.clone(); + match apply_changes(shortcuts, &env_name, &mut project) + .await + .wrap_err_with(|| { + format!( + "Couldn't remove shortcuts from {}", + env_name.fancy_display() + ) + }) { + Ok(state_changes) => { + state_changes.report(); + } + Err(err) => { + if let Err(revert_err) = + revert_environment_after_error(&env_name, &last_updated_project).await + { + tracing::warn!("Reverting of the operation failed"); + tracing::info!("Reversion error: {:?}", revert_err); + } + return Err(err); + } + } + last_updated_project = project; + } + Ok(()) +} diff --git a/src/global/list.rs b/src/global/list.rs index 4e62b1fd64..6b78a45896 100644 --- a/src/global/list.rs +++ b/src/global/list.rs @@ -30,13 +30,13 @@ pub fn format_asciiart_section(label: &str, content: String, last: bool, more: b format!("\n{} {}─ {}: {}", prefix, symbol, label, content) } -pub fn format_exposed(exposed: &IndexSet, last: bool) -> Option { +pub fn format_exposed(exposed: &IndexSet, last: bool, more: bool) -> Option { if exposed.is_empty() { Some(format_asciiart_section( "exposes", console::style("Nothing").dim().red().to_string(), last, - false, + more, )) } else { let formatted_exposed = exposed.iter().map(format_mapping).join(", "); @@ -44,7 +44,7 @@ pub fn format_exposed(exposed: &IndexSet, last: bool) -> Option "exposes", formatted_exposed, last, - false, + more, )) } } @@ -257,11 +257,24 @@ pub async fn list_all_global_environments( message.push_str(&dep_message); } + // Check for shortcuts + let shortcuts = env.shortcuts.clone().unwrap_or_else(IndexSet::new); + // Write exposed binaries - if let Some(exp_message) = format_exposed(&env.exposed, last) { + if let Some(exp_message) = format_exposed(&env.exposed, last, !shortcuts.is_empty()) { message.push_str(&exp_message); } + // Write shortcuts + if !shortcuts.is_empty() { + message.push_str(&format_asciiart_section( + "shortcuts", + shortcuts.iter().map(PackageName::as_normalized).join(", "), + last, + false, + )); + } + if !last { message.push('\n'); } diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index 3130ee45ab..448be7e1f7 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -488,12 +488,68 @@ impl Manifest { *shortcuts_array = existing_shortcuts.iter().collect(); tracing::debug!( - "Added channel {} for environment {env_name} in toml document", - console::style(shortcut.as_normalized()).green() + "Added shortcut {} for environment {} in toml document", + console::style(shortcut.as_normalized()).green(), + env_name.fancy_display() ); Ok(()) } + /// Removes shortcut from the manifest of any environment + pub fn remove_shortcut( + &mut self, + shortcut: &PackageName, + env_name: &EnvironmentName, + ) -> miette::Result<()> { + // Ensure the environment exists + if !self.parsed.envs.contains_key(env_name) { + miette::bail!("Environment {} doesn't exist", env_name.fancy_display()); + } + let environment = self + .parsed + .envs + .get_mut(env_name) + .ok_or_else(|| miette::miette!("[envs.{env_name}] needs to exist"))?; + + // Remove shortcut from parsed environment + if let Some(shortcuts) = environment.shortcuts.as_mut() { + if !shortcuts.contains(shortcut) { + miette::bail!("The shortcut {} doesn't exist", shortcut.as_normalized()); + } + + shortcuts.swap_remove(shortcut); + tracing::debug!( + "Removed shortcut '{}' from toml document", + shortcut.as_normalized() + ); + } + + // Remove from the document + let env_key = format!("envs.{env_name}"); + let shortcuts_array = self + .document + .get_mut_toml_array(&env_key, "shortcuts")? + .ok_or_else(|| miette::miette!("No shortcuts found for environment {}", env_name))?; + + let shortcut_str = shortcut.as_normalized(); + // First find the index without holding onto the iterator + let maybe_index = shortcuts_array + .iter() + .position(|item| item.as_str() == Some(shortcut_str)); + + if let Some(index) = maybe_index { + shortcuts_array.remove(index); + tracing::debug!("Removed shortcut '{}' from toml document", shortcut_str); + } else { + return Err(miette::miette!( + "The shortcut '{}' doesn't exist", + shortcut_str + )); + } + + Ok(()) + } + /// Saves the manifest to the file system pub async fn save(&self) -> miette::Result<()> { let contents = { diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index d81cd651e8..a8cf5a5ee1 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -1141,7 +1141,7 @@ impl Project { } for record in records_to_uninstall { - rattler_menuinst::remove_menu_items(&record.installed_system_menus) + rattler_menuinst::remove_menuitems_for_record(prefix.root(), record.clone()) .into_diagnostic()?; state_changes.insert_change( diff --git a/tests/integration_python/pixi_global/test_shortcuts.py b/tests/integration_python/pixi_global/test_shortcuts.py index dd8a6a94ca..c625f9af95 100644 --- a/tests/integration_python/pixi_global/test_shortcuts.py +++ b/tests/integration_python/pixi_global/test_shortcuts.py @@ -303,4 +303,78 @@ def test_install_no_shortcut( verify_shortcuts_exist(setup_data.data_home, ["pixi-editor"], expected_exists=False) -# TODO: test more files on macOS and Windows +def test_remove_shortcut( + pixi: Path, + setup_data: SetupData, + shortcuts_channel_1: str, +) -> None: + # Verify no shortcuts exist after sync + verify_cli_command( + [pixi, "global", "install", "--channel", shortcuts_channel_1, "pixi-editor"], + ExitCode.SUCCESS, + env=setup_data.env, + ) + + # Verify manifest + manifest = setup_data.pixi_home.joinpath("manifests", "pixi-global.toml") + parsed_toml = tomllib.loads(manifest.read_text()) + assert parsed_toml["envs"]["pixi-editor"]["shortcuts"] == ["pixi-editor"] + + # Verify shortcut exists + verify_shortcuts_exist(setup_data.data_home, ["pixi-editor"], expected_exists=True) + + # Remove shortcut + verify_cli_command( + [pixi, "global", "shortcut", "remove", "pixi-editor"], + ExitCode.SUCCESS, + env=setup_data.env, + ) + + # Verify removal from manifest + parsed_toml = tomllib.loads(manifest.read_text()) + assert parsed_toml["envs"]["pixi-editor"]["shortcuts"] != ["pixi-editor"] + + # Verify shortcut does not exist + verify_shortcuts_exist(setup_data.data_home, ["pixi-editor"], expected_exists=False) + + +def test_add_shortcut( + pixi: Path, + setup_data: SetupData, + shortcuts_channel_1: str, +) -> None: + verify_cli_command( + [ + pixi, + "global", + "install", + "--no-shortcuts", + "--channel", + shortcuts_channel_1, + "pixi-editor", + ], + ExitCode.SUCCESS, + env=setup_data.env, + ) + + # Verify manifest + manifest = setup_data.pixi_home.joinpath("manifests", "pixi-global.toml") + parsed_toml = tomllib.loads(manifest.read_text()) + assert parsed_toml["envs"]["pixi-editor"].get("shortcuts") is None + + # Verify shortcut exists + verify_shortcuts_exist(setup_data.data_home, ["pixi-editor"], expected_exists=False) + + # Add shortcut + verify_cli_command( + [pixi, "global", "shortcut", "add", "pixi-editor", "--environment", "pixi-editor"], + ExitCode.SUCCESS, + env=setup_data.env, + ) + + # Verify addition to manifest + parsed_toml = tomllib.loads(manifest.read_text()) + assert parsed_toml["envs"]["pixi-editor"]["shortcuts"] == ["pixi-editor"] + + # Verify shortcut exists + verify_shortcuts_exist(setup_data.data_home, ["pixi-editor"], expected_exists=True)