summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorColin J. Fuller <cjfuller@gmail.com>2023-09-11 03:36:12 -0400
committerGitHub <noreply@github.com>2023-09-11 09:36:12 +0200
commit40b479ebaa7318d48ca9a15edbd072d42897574e (patch)
tree5a8303df95cb2e16e6597950432d8093ce725635
parent8464f37342830392807123625ff37286254c4aa0 (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.rs167
-rw-r--r--src/cli/global/list.rs114
-rw-r--r--src/cli/global/mod.rs8
-rw-r--r--src/cli/global/remove.rs108
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(())
+}