Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions docs/reference/cli/pixi/global.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pixi global <COMMAND>
| [`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 |


Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli/pixi/global/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pixi global install [OPTIONS] <PACKAGES>...
: Add additional dependencies to the environment. Their executables will not be exposed
- <a id="arg---force-reinstall" href="#arg---force-reinstall">`--force-reinstall`</a>
: Specifies that the environment should be reinstalled
- <a id="arg---no-shortcut" href="#arg---no-shortcut">`--no-shortcut`</a>
- <a id="arg---no-shortcuts" href="#arg---no-shortcuts">`--no-shortcuts`</a>
: Specifies that no shortcuts should be created for the installed packages

## Config Options
Expand Down
21 changes: 21 additions & 0 deletions docs/reference/cli/pixi/global/shortcut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!--- This file is autogenerated. Do not edit manually! -->
# <code>[pixi](../../pixi.md) [global](../global.md) shortcut</code>

## About
Interact with the shortcuts on your machine

--8<-- "docs/reference/cli/pixi/global/shortcut_extender.md:description"

## Usage
```
pixi global shortcut <COMMAND>
```

## 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"
36 changes: 36 additions & 0 deletions docs/reference/cli/pixi/global/shortcut/add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!--- This file is autogenerated. Do not edit manually! -->
# <code>[pixi](../../../pixi.md) [global](../../global.md) [shortcut](../shortcut.md) add</code>

## 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 <ENVIRONMENT> [PACKAGE]...
```

## Arguments
- <a id="arg-<PACKAGE>" href="#arg-<PACKAGE>">`<PACKAGE>`</a>
: The package name to add the shortcuts from

## Options
- <a id="arg---environment" href="#arg---environment">`--environment (-e) <ENVIRONMENT>`</a>
: The environment from which the shortcut should be added
<br>**required**: `true`

## Config Options
- <a id="arg---tls-no-verify" href="#arg---tls-no-verify">`--tls-no-verify`</a>
: Do not verify the TLS certificate of the server
- <a id="arg---auth-file" href="#arg---auth-file">`--auth-file <AUTH_FILE>`</a>
: Path to the file containing the authentication token
- <a id="arg---pypi-keyring-provider" href="#arg---pypi-keyring-provider">`--pypi-keyring-provider <PYPI_KEYRING_PROVIDER>`</a>
: Specifies whether to use the keyring to look up credentials for PyPI
<br>**options**: `disabled`, `subprocess`
- <a id="arg---concurrent-solves" href="#arg---concurrent-solves">`--concurrent-solves <CONCURRENT_SOLVES>`</a>
: Max concurrent solves, default is the number of CPUs
- <a id="arg---concurrent-downloads" href="#arg---concurrent-downloads">`--concurrent-downloads <CONCURRENT_DOWNLOADS>`</a>
: Max concurrent network requests, default is `50`

--8<-- "docs/reference/cli/pixi/global/shortcut/add_extender.md:example"
31 changes: 31 additions & 0 deletions docs/reference/cli/pixi/global/shortcut/remove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!--- This file is autogenerated. Do not edit manually! -->
# <code>[pixi](../../../pixi.md) [global](../../global.md) [shortcut](../shortcut.md) remove</code>

## About
Remove shortcuts from your machine

--8<-- "docs/reference/cli/pixi/global/shortcut/remove_extender.md:description"

## Usage
```
pixi global shortcut remove [OPTIONS] [SHORTCUTS]...
```

## Arguments
- <a id="arg-<SHORTCUTS>" href="#arg-<SHORTCUTS>">`<SHORTCUTS>`</a>
: The shortcuts that should be removed

## Config Options
- <a id="arg---tls-no-verify" href="#arg---tls-no-verify">`--tls-no-verify`</a>
: Do not verify the TLS certificate of the server
- <a id="arg---auth-file" href="#arg---auth-file">`--auth-file <AUTH_FILE>`</a>
: Path to the file containing the authentication token
- <a id="arg---pypi-keyring-provider" href="#arg---pypi-keyring-provider">`--pypi-keyring-provider <PYPI_KEYRING_PROVIDER>`</a>
: Specifies whether to use the keyring to look up credentials for PyPI
<br>**options**: `disabled`, `subprocess`
- <a id="arg---concurrent-solves" href="#arg---concurrent-solves">`--concurrent-solves <CONCURRENT_SOLVES>`</a>
: Max concurrent solves, default is the number of CPUs
- <a id="arg---concurrent-downloads" href="#arg---concurrent-downloads">`--concurrent-downloads <CONCURRENT_DOWNLOADS>`</a>
: Max concurrent network requests, default is `50`

--8<-- "docs/reference/cli/pixi/global/shortcut/remove_extender.md:example"
6 changes: 3 additions & 3 deletions src/cli/global/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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?;
Expand Down
4 changes: 4 additions & 0 deletions src/cli/global/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod expose;
mod install;
mod list;
mod remove;
mod shortcut;
mod sync;
mod uninstall;
mod update;
Expand All @@ -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),
Expand Down Expand Up @@ -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?,
Expand Down
186 changes: 186 additions & 0 deletions src/cli/global/shortcut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
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<PackageName>,

/// The environment from which the shortcut should be added
Comment thread
ruben-arts marked this conversation as resolved.
Outdated
#[clap(short, long)]
environment: EnvironmentName,

#[clap(flatten)]
config: ConfigCli,
}

/// Remove shortcuts from your machine.
#[derive(Parser, Debug)]
pub struct RemoveArgs {
/// The shortcuts that should be removed
#[arg(num_args = 1..)]
shortcuts: Vec<PackageName>,
Comment thread
ruben-arts marked this conversation as resolved.
Outdated

#[clap(flatten)]
config: ConfigCli,
}

/// Interact with the shortcuts on your machine
Comment thread
ruben-arts marked this conversation as resolved.
Outdated
#[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<StateChanges, miette::Error> {
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?;
project.manifest.save().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<PackageName>,
env_name: &EnvironmentName,
project: &mut Project,
) -> Result<StateChanges, miette::Error> {
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<EnvironmentName, Vec<PackageName>> = 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::<Vec<_>>();
(!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::<Vec<_>>()
.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(())
}
21 changes: 17 additions & 4 deletions src/global/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@ 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<Mapping>, last: bool) -> Option<String> {
pub fn format_exposed(exposed: &IndexSet<Mapping>, last: bool, more: bool) -> Option<String> {
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(", ");
Some(format_asciiart_section(
"exposes",
formatted_exposed,
last,
false,
more,
))
}
}
Expand Down Expand Up @@ -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');
}
Expand Down
Loading