diff options
author | Colin J. Fuller <cjfuller@gmail.com> | 2023-09-11 03:36:12 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-11 09:36:12 +0200 |
commit | 40b479ebaa7318d48ca9a15edbd072d42897574e (patch) | |
tree | 5a8303df95cb2e16e6597950432d8093ce725635 | |
parent | 8464f37342830392807123625ff37286254c4aa0 (diff) |
Add `pixi global list` and `pixi global remove` commands (#318)
* Add a `global remove` command
* Add a `global list` command
* Fix lint; run cargo fmt
* Review fixes: destructure BinEnv[Dir], command docstrings
* A couple more Bin[Env]Dir destructures
* Remove dry_run, split out path construction / script generation
-rw-r--r-- | src/cli/global/install.rs | 167 | ||||
-rw-r--r-- | src/cli/global/list.rs | 114 | ||||
-rw-r--r-- | src/cli/global/mod.rs | 8 | ||||
-rw-r--r-- | src/cli/global/remove.rs | 108 |
4 files changed, 360 insertions, 37 deletions
diff --git a/src/cli/global/install.rs b/src/cli/global/install.rs index 051956c..075e0ea 100644 --- a/src/cli/global/install.rs +++ b/src/cli/global/install.rs @@ -44,7 +44,7 @@ pub struct Args { channel: Vec<String>, } -struct BinDir(pub PathBuf); +pub(crate) struct BinDir(pub PathBuf); impl BinDir { /// Create the Binary Executable directory @@ -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 @@ -64,12 +76,32 @@ fn bin_dir() -> miette::Result<PathBuf> { .join(BIN_DIR)) } -struct BinEnvDir(pub PathBuf); +pub(crate) struct BinEnvDir(pub PathBuf); impl BinEnvDir { + /// Construct the path to the env directory for the binary package `package_name`. + fn package_bin_env_dir(package_name: &str) -> miette::Result<PathBuf> { + 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()?; @@ -78,14 +110,14 @@ impl BinEnvDir { } /// Binary environments are installed in ~/.pixi/envs -fn bin_env_dir() -> miette::Result<PathBuf> { +pub(crate) fn bin_env_dir() -> miette::Result<PathBuf> { Ok(home_dir() .ok_or_else(|| miette::miette!("could not find home directory"))? .join(BIN_ENVS_DIR)) } /// 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> { @@ -97,7 +129,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 @@ -150,22 +185,73 @@ fn is_executable(prefix: &Prefix, relative_path: &Path) -> bool { is_executable::is_executable(absolute_path) } +/// Find the executable scripts within the specified package installed in this conda prefix. +fn find_executables<'a>(prefix: &Prefix, prefix_package: &'a PrefixRecord) -> Vec<&'a Path> { + prefix_package + .files + .iter() + .filter(|relative_path| is_executable(prefix, relative_path)) + .map(|buf| buf.as_ref()) + .collect() +} + +/// Mapping from an executable in a package environment to its global binary script location. +#[derive(Debug)] +pub(crate) struct BinScriptMapping<'a> { + pub original_executable: &'a Path, + pub global_binary_path: PathBuf, +} + +/// For each executable provided, map it to the installation path for its global binary script. +async fn map_executables_to_global_bin_scripts<'a>( + package_executables: &[&'a Path], + bin_dir: &BinDir, +) -> miette::Result<Vec<BinScriptMapping<'a>>> { + let BinDir(bin_dir) = bin_dir; + let mut mappings = vec![]; + for exec in package_executables.iter() { + let file_name = exec + .file_stem() + .ok_or_else(|| miette::miette!("could not get filename from {}", exec.display()))?; + let mut executable_script_path = bin_dir.join(file_name); + + if cfg!(windows) { + executable_script_path.set_extension("bat"); + }; + mappings.push(BinScriptMapping { + original_executable: exec, + global_binary_path: executable_script_path, + }); + } + Ok(mappings) +} + +/// Find all executable scripts in a package and map them to their global install paths. +/// +/// (Convenience wrapper around `find_executables` and `map_executables_to_global_bin_scripts` which +/// are generally used together.) +pub(crate) async fn find_and_map_executable_scripts<'a>( + prefix: &Prefix, + prefix_package: &'a PrefixRecord, + bin_dir: &BinDir, +) -> miette::Result<Vec<BinScriptMapping<'a>>> { + let executables = find_executables(prefix, prefix_package); + map_executables_to_global_bin_scripts(&executables, bin_dir).await +} + /// 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. +pub(crate) async fn create_executable_scripts( + mapped_executables: &[BinScriptMapping<'_>], prefix: &Prefix, - prefix_package: &PrefixRecord, shell: &ShellEnum, activation_script: String, -) -> miette::Result<Vec<String>> { - let executables = prefix_package - .files - .iter() - .filter(|relative_path| is_executable(prefix, relative_path)); - - let mut scripts = Vec::new(); - let bin_dir = BinDir::create().await?; - for exec in executables { +) -> miette::Result<()> { + for BinScriptMapping { + original_executable: exec, + global_binary_path: executable_script_path, + } in mapped_executables + { let mut script = activation_script.clone(); shell .run_command( @@ -176,16 +262,6 @@ async fn create_executable_scripts( ], ) .expect("should never fail"); - - let file_name = exec - .file_stem() - .ok_or_else(|| miette::miette!("could not get filename from {}", exec.display()))?; - let mut executable_script_path = bin_dir.0.join(file_name); - - if cfg!(windows) { - executable_script_path.set_extension("bat"); - }; - tokio::fs::write(&executable_script_path, script) .await .into_diagnostic()?; @@ -199,10 +275,8 @@ async fn create_executable_scripts( ) .into_diagnostic()?; } - - scripts.push(file_name.to_string_lossy().into_owned()); } - Ok(scripts) + Ok(()) } /// Install a global command @@ -257,8 +331,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { let records = libsolv_rs::Solver.solve(task).into_diagnostic()?; // Create the binary environment prefix where we install or update the package - let bin_prefix = BinEnvDir::create(&package_name).await?; - let prefix = Prefix::new(bin_prefix.0)?; + let BinEnvDir(bin_prefix) = BinEnvDir::create(&package_name).await?; + let prefix = Prefix::new(bin_prefix)?; let prefix_records = prefix.find_installed_packages(None).await?; // Create the transaction that we need @@ -298,11 +372,23 @@ 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 bin_dir = BinDir::create().await?; + let script_mapping = + find_and_map_executable_scripts(&prefix, &prefix_package, &bin_dir).await?; + create_executable_scripts(&script_mapping, &prefix, &shell, activation_script).await?; + + let scripts: Vec<_> = script_mapping + .into_iter() + .map( + |BinScriptMapping { + global_binary_path: path, + .. + }| path, + ) + .collect(); // 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(), @@ -321,8 +407,15 @@ pub async fn execute(args: Args) -> miette::Result<()> { channel, ); - let script_names = script_names + 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() { diff --git a/src/cli/global/list.rs b/src/cli/global/list.rs new file mode 100644 index 0000000..992d527 --- /dev/null +++ b/src/cli/global/list.rs @@ -0,0 +1,114 @@ +use std::collections::HashSet; +use std::fmt::Display; + +use clap::Parser; +use itertools::Itertools; +use miette::IntoDiagnostic; + +use crate::cli::global::install::{ + bin_env_dir, find_and_map_executable_scripts, find_designated_package, BinDir, BinEnvDir, + BinScriptMapping, +}; +use crate::prefix::Prefix; + +/// Lists all packages previously installed into a globally accessible location via `pixi global install`. +#[derive(Parser, Debug)] +pub struct Args {} + +#[derive(Debug)] +struct InstalledPackageInfo { + name: String, + binaries: Vec<String>, +} + +impl Display for InstalledPackageInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let binaries = self + .binaries + .iter() + .map(|name| format!("[bin] {}", console::style(name).bold())) + .join("\n - "); + write!( + f, + " - [package] {}\n - {binaries}", + console::style(&self.name).bold() + ) + } +} + +fn print_no_packages_found_message() { + eprintln!( + "{} No globally installed binaries found", + console::style("!").yellow().bold() + ) +} + +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() { + packages.push(entry.file_name().to_string_lossy().to_string()); + } + } + + let mut package_info = vec![]; + + for package_name in packages { + let Ok(BinEnvDir(bin_env_prefix)) = BinEnvDir::from_existing(&package_name).await else { + print_no_packages_found_message(); + return Ok(()); + }; + let prefix = Prefix::new(bin_env_prefix)?; + + let Ok(bin_prefix) = BinDir::from_existing().await else { + print_no_packages_found_message(); + return Ok(()); + }; + + // Find the installed package in the environment + let prefix_package = find_designated_package(&prefix, &package_name).await?; + + let binaries: Vec<_> = + find_and_map_executable_scripts(&prefix, &prefix_package, &bin_prefix) + .await? + .into_iter() + .map( + |BinScriptMapping { + global_binary_path: path, + .. + }| { + path.strip_prefix(&bin_prefix.0) + .expect("script paths were constructed by joining onto BinDir") + .to_string_lossy() + .to_string() + }, + ) + // Collecting to a HashSet first is a workaround for issue #317 and can be removed + // once that is fixed. + .collect::<HashSet<_>>() + .into_iter() + .collect(); + + package_info.push(InstalledPackageInfo { + name: package_name, + binaries, + }); + } + + if package_info.is_empty() { + print_no_packages_found_message(); + } else { + eprintln!( + "Globally installed binary packages:\n{}", + package_info + .into_iter() + .map(|pkg| pkg.to_string()) + .join("\n") + ); + } + + Ok(()) +} diff --git a/src/cli/global/mod.rs b/src/cli/global/mod.rs index a7e8b73..c71e07a 100644 --- a/src/cli/global/mod.rs +++ b/src/cli/global/mod.rs @@ -1,10 +1,16 @@ use clap::Parser; mod install; +mod list; +mod remove; #[derive(Debug, Parser)] pub enum Command { #[clap(alias = "a")] Install(install::Args), + #[clap(alias = "r")] + Remove(remove::Args), + #[clap(alias = "ls")] + List(list::Args), } /// Global is the main entry point for the part of pixi that executes on the global(system) level. @@ -19,6 +25,8 @@ 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?, + Command::List(args) => list::execute(args).await?, }; Ok(()) } diff --git a/src/cli/global/remove.rs b/src/cli/global/remove.rs new file mode 100644 index 0000000..ff2b19f --- /dev/null +++ b/src/cli/global/remove.rs @@ -0,0 +1,108 @@ +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 crate::cli::global::install::{ + find_and_map_executable_scripts, find_designated_package, BinDir, BinEnvDir, BinScriptMapping, +}; +use crate::prefix::Prefix; + +/// Removes a package previously installed into a globally accessible location via `pixi global install`. +#[derive(Parser, Debug)] +#[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 BinEnvDir(bin_prefix) = BinEnvDir::from_existing(&package_name).await?; + let prefix = Prefix::new(bin_prefix.clone())?; + + // Find the installed package in the environment + let prefix_package = find_designated_package(&prefix, &package_name).await?; + + // Construct the paths to all the installed package executables, which are what we need to remove. + let paths_to_remove: Vec<_> = + find_and_map_executable_scripts(&prefix, &prefix_package, &BinDir::from_existing().await?) + .await? + .into_iter() + .map( + |BinScriptMapping { + global_binary_path: path, + .. + }| path, + ) + // Collecting to a HashSet first is a workaround for issue #317 and can be removed + // once that is fixed. + .collect::<HashSet<_>>() + .into_iter() + .collect(); + + let dirs_to_remove: Vec<_> = vec![bin_prefix]; + + 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(()) +} |