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

Add pixi global list and pixi global remove commands #318

Merged
merged 6 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
96 changes: 72 additions & 24 deletions src/cli/global/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ impl BinDir {
.into_diagnostic()?;
Ok(Self(bin_dir))
}

/// Get the Binary Executable directory, erroring if it doesn't already exist.
pub async fn from_existing() -> miette::Result<Self> {
let bin_dir = bin_dir()?;
if tokio::fs::try_exists(&bin_dir).await.into_diagnostic()? {
Ok(Self(bin_dir))
} else {
Err(miette::miette!(
"binary executable directory does not exist"
))
}
}
}

/// Binaries are installed in ~/.pixi/bin
Expand All @@ -64,12 +76,31 @@ fn bin_dir() -> miette::Result<PathBuf> {
.join(BIN_DIR))
}

struct BinEnvDir(pub PathBuf);
pub(crate) struct BinEnvDir(pub PathBuf);

impl BinEnvDir {
fn package_bin_env_dir(package_name: &str) -> miette::Result<PathBuf> {
baszalmstra marked this conversation as resolved.
Show resolved Hide resolved
Ok(bin_env_dir()?.join(package_name))
}

/// Get the Binary Environment directory, erroring if it doesn't already exist.
pub async fn from_existing(package_name: &str) -> miette::Result<Self> {
let bin_env_dir = Self::package_bin_env_dir(package_name)?;
if tokio::fs::try_exists(&bin_env_dir)
.await
.into_diagnostic()?
{
Ok(Self(bin_env_dir))
} else {
Err(miette::miette!(
"could not find environment for package {package_name}"
))
}
}

/// Create the Binary Environment directory
pub async fn create(package_name: &str) -> miette::Result<Self> {
let bin_env_dir = bin_env_dir()?.join(package_name);
let bin_env_dir = Self::package_bin_env_dir(package_name)?;
tokio::fs::create_dir_all(&bin_env_dir)
.await
.into_diagnostic()?;
Expand All @@ -85,7 +116,7 @@ fn bin_env_dir() -> miette::Result<PathBuf> {
}

/// Find the designated package in the prefix
async fn find_designated_package(
pub(crate) async fn find_designated_package(
prefix: &Prefix,
package_name: &str,
) -> miette::Result<PrefixRecord> {
Expand All @@ -97,7 +128,10 @@ async fn find_designated_package(
}

/// Create the environment activation script
fn create_activation_script(prefix: &Prefix, shell: ShellEnum) -> miette::Result<String> {
pub(crate) fn create_activation_script(
prefix: &Prefix,
shell: ShellEnum,
) -> miette::Result<String> {
let activator =
Activator::from_path(prefix.root(), shell, Platform::Osx64).into_diagnostic()?;
let result = activator
Expand Down Expand Up @@ -151,13 +185,17 @@ fn is_executable(prefix: &Prefix, relative_path: &Path) -> bool {
}

/// Create the executable scripts by modifying the activation script
/// to activate the environment and run the executable
async fn create_executable_scripts(
/// to activate the environment and run the executable.
///
/// If `dry_run` is true, return the filenames of the scripts that would be created but don't
/// actually write them to disk.
pub(crate) async fn create_executable_scripts(
baszalmstra marked this conversation as resolved.
Show resolved Hide resolved
prefix: &Prefix,
prefix_package: &PrefixRecord,
shell: &ShellEnum,
activation_script: String,
) -> miette::Result<Vec<String>> {
dry_run: bool,
) -> miette::Result<Vec<PathBuf>> {
let executables = prefix_package
.files
.iter()
Expand Down Expand Up @@ -186,21 +224,23 @@ async fn create_executable_scripts(
executable_script_path.set_extension("bat");
};

tokio::fs::write(&executable_script_path, script)
.await
.into_diagnostic()?;

#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(
executable_script_path,
std::fs::Permissions::from_mode(0o744),
)
.into_diagnostic()?;
if !dry_run {
tokio::fs::write(&executable_script_path, script)
.await
.into_diagnostic()?;

#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(
&executable_script_path,
std::fs::Permissions::from_mode(0o744),
)
.into_diagnostic()?;
}
}

scripts.push(file_name.to_string_lossy().into_owned());
scripts.push(executable_script_path);
}
Ok(scripts)
}
Expand Down Expand Up @@ -298,11 +338,12 @@ pub async fn execute(args: Args) -> miette::Result<()> {
// Construct the reusable activation script for the shell and generate an invocation script
// for each executable added by the package to the environment.
let activation_script = create_activation_script(&prefix, shell.clone())?;
let script_names =
create_executable_scripts(&prefix, &prefix_package, &shell, activation_script).await?;
let scripts =
create_executable_scripts(&prefix, &prefix_package, &shell, activation_script, false)
.await?;

// Check if the bin path is on the path
if script_names.is_empty() {
if scripts.is_empty() {
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).bold(),
Expand All @@ -321,8 +362,15 @@ pub async fn execute(args: Args) -> miette::Result<()> {
channel,
);

let script_names = script_names
let bin_dir = BinDir::from_existing().await?;
let script_names = scripts
.into_iter()
.map(|path| {
path.strip_prefix(&bin_dir.0)
.expect("script paths were constructed by joining onto BinDir")
.to_string_lossy()
.to_string()
})
.join(&format!("\n{whitespace} - "));

if is_bin_folder_on_path() {
Expand Down
4 changes: 4 additions & 0 deletions src/cli/global/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use clap::Parser;
mod install;
mod remove;

#[derive(Debug, Parser)]
pub enum Command {
#[clap(alias = "a")]
Install(install::Args),
#[clap(alias = "r")]
Remove(remove::Args),
}

/// Global is the main entry point for the part of pixi that executes on the global(system) level.
Expand All @@ -19,6 +22,7 @@ pub struct Args {
pub async fn execute(cmd: Args) -> miette::Result<()> {
match cmd.command {
Command::Install(args) => install::execute(args).await?,
Command::Remove(args) => remove::execute(args).await?,
};
Ok(())
}
111 changes: 111 additions & 0 deletions src/cli/global/remove.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use std::collections::HashSet;
use std::str::FromStr;

use clap::Parser;
use clap_verbosity_flag::{Level, Verbosity};
use itertools::Itertools;
use miette::IntoDiagnostic;
use rattler_conda_types::MatchSpec;
use rattler_shell::shell::ShellEnum;

use crate::cli::global::install::{
create_activation_script, create_executable_scripts, find_designated_package, BinEnvDir,
};
use crate::prefix::Prefix;

#[derive(Parser, Debug)]
baszalmstra marked this conversation as resolved.
Show resolved Hide resolved
#[clap(arg_required_else_help = true)]
pub struct Args {
/// Specifies the package that is to be removed.
package: String,
#[command(flatten)]
verbose: Verbosity,
}

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 bin_prefix = BinEnvDir::from_existing(&package_name).await?;
baszalmstra marked this conversation as resolved.
Show resolved Hide resolved
let prefix = Prefix::new(bin_prefix.0.clone())?;

// Find the installed package in the environment
let prefix_package = find_designated_package(&prefix, &package_name).await?;

// Determine the shell to use for the invocation script
let shell: ShellEnum = if cfg!(windows) {
rattler_shell::shell::CmdExe.into()
} else {
rattler_shell::shell::Bash.into()
};

// Construct the reusable activation script for the shell and generate an invocation script
// for each executable added by the package to the environment.
let activation_script = create_activation_script(&prefix, shell.clone())?;
let paths_to_remove: Vec<_> =
create_executable_scripts(&prefix, &prefix_package, &shell, activation_script, true)
.await?
// Collecting to a HashSet first is a workaround for issue #317 and can be removed
// once that is fixed.
.into_iter()
.collect::<HashSet<_>>()
.into_iter()
.collect();

let dirs_to_remove: Vec<_> = vec![bin_prefix.0];

if args.verbose.log_level().unwrap_or(Level::Error) >= Level::Warn {
let whitespace = console::Emoji(" ", "").to_string();
let names_to_remove = dirs_to_remove
.iter()
.map(|dir| dir.to_string_lossy())
.chain(paths_to_remove.iter().map(|path| path.to_string_lossy()))
.join(&format!("\n{whitespace} - "));

eprintln!(
"{} Removing the following files and directories:\n{whitespace} - {names_to_remove}",
console::style("!").yellow().bold(),
)
}

let mut errors = vec![];

for file in paths_to_remove {
if let Err(e) = tokio::fs::remove_file(&file).await.into_diagnostic() {
errors.push((file, e))
}
}

for dir in dirs_to_remove {
if let Err(e) = tokio::fs::remove_dir_all(&dir).await.into_diagnostic() {
errors.push((dir, e))
}
}

if errors.is_empty() {
eprintln!(
"{}Successfully removed global package {}",
console::style(console::Emoji("✔ ", "")).green(),
console::style(package_name).bold(),
);
} else {
let whitespace = console::Emoji(" ", "").to_string();
let error_string = errors
.into_iter()
.map(|(file, e)| format!("{} (on {})", e, file.to_string_lossy()))
.join(&format!("\n{whitespace} - "));
miette::bail!(
"got multiple errors trying to remove global package {}:\n{} - {}",
package_name,
whitespace,
error_string,
);
}

Ok(())
}