use anyhow::Result;
use clap::{ArgAction, Parser, Subcommand};
use inquire::Confirm;
use serde::{Deserialize, Serialize};
use std::io::Read;
use std::io::Write;
use std::path::PathBuf;
use tracing::Level;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
#[arg(long, short, action = ArgAction::Count, default_value = "0")]
verbosity: u8,
#[clap(subcommand)]
pub command: Option<Command>,
}
#[derive(Subcommand, Debug)]
pub enum Command {
Hoist {
#[clap(long, short)]
binaries: Option<Vec<String>>,
},
List,
Install {
#[clap(long, short)]
binaries: Option<Vec<String>>,
},
}
pub const INSTALL_BASH_FUNCTION: &str = r#"
function cargo() {
if ~/.cargo/bin/cargo hoist --help &>/dev/null; then
~/.cargo/bin/cargo hoist install
fi
~/.cargo/bin/cargo "$@"
}
"#;
pub fn run() -> Result<()> {
let Args { verbosity, command } = Args::parse();
init_tracing_subscriber(verbosity)?;
HoistRegistry::create_pre_hook(true)?;
match command {
None => HoistRegistry::install(None),
Some(c) => match c {
Command::Hoist { binaries } => HoistRegistry::hoist(binaries),
Command::List => HoistRegistry::list(),
Command::Install { binaries } => HoistRegistry::install(binaries),
},
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct HoistedBinary {
pub name: String,
pub location: PathBuf,
}
impl HoistedBinary {
pub fn new(name: String, location: PathBuf) -> Self {
Self { name, location }
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct HoistRegistry {
pub binaries: Vec<HoistedBinary>,
}
impl HoistRegistry {
pub fn dir() -> Result<PathBuf> {
let hoist_dir = std::env::var("HOME")? + "/.hoist/";
Ok(PathBuf::from(hoist_dir))
}
pub fn path() -> Result<PathBuf> {
let hoist_dir = HoistRegistry::dir()?;
Ok(hoist_dir.join("registry.toml"))
}
pub fn hook_identifier() -> Result<PathBuf> {
let hoist_dir = HoistRegistry::dir()?;
Ok(hoist_dir.join("hook"))
}
pub fn create_dir() -> Result<()> {
let hoist_dir = HoistRegistry::dir()?;
if !std::path::Path::new(&hoist_dir).exists() {
tracing::info!("Creating ~/.hoist/ directory");
std::fs::create_dir(hoist_dir)?;
}
Ok(())
}
pub fn create_registry() -> Result<()> {
HoistRegistry::create_dir()?;
let registry_file = HoistRegistry::path()?;
if !std::path::Path::new(®istry_file).exists() {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(registry_file)?;
let default_registry = HoistRegistry::default();
let toml = toml::to_string(&default_registry)?;
file.write_all(toml.as_bytes())?;
}
Ok(())
}
pub fn create_pre_hook(with_confirm: bool) -> Result<()> {
HoistRegistry::create_dir()?;
let hook_file = HoistRegistry::hook_identifier()?;
if !std::path::Path::new(&hook_file).exists() {
if with_confirm && !Confirm::new("Cargo hoist pre-cargo hook not installed. Do you want to install? ([y]/n) Once installed, this prompt will not bother you again :)").prompt()? {
anyhow::bail!("cargo hoist installation rejected");
}
let bash_file = std::env::var("HOME")? + "/.bashrc";
if !std::path::Path::new(&bash_file).exists() {
anyhow::bail!("~/.bashrc file does not exist");
}
let mut file = std::fs::OpenOptions::new().append(true).open(bash_file)?;
file.write_all(INSTALL_BASH_FUNCTION.as_bytes())?;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(hook_file)?;
file.write_all("hook".as_bytes())?;
}
Ok(())
}
pub fn setup() -> Result<()> {
HoistRegistry::create_dir()?;
HoistRegistry::create_registry()?;
HoistRegistry::create_pre_hook(false)?;
Ok(())
}
pub fn install(binaries: Option<Vec<String>>) -> Result<()> {
HoistRegistry::setup()?;
let registry_file = HoistRegistry::path()?;
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(registry_file)?;
let mut registry_toml = String::new();
file.read_to_string(&mut registry_toml)?;
let mut registry: HoistRegistry = toml::from_str(®istry_toml)?;
let binaries = binaries.unwrap_or_default();
for binary in binaries {
let binary_path = std::env::current_dir()?.join("target/debug/").join(binary);
let binary_path = binary_path.canonicalize()?;
let bin_file_name = binary_path
.file_name()
.ok_or(anyhow::anyhow!("[std] failed to extract binary name"))?;
let binary_name = bin_file_name
.to_str()
.ok_or(anyhow::anyhow!(
"[std] failed to convert binary path name to string"
))?
.to_string();
let binary = HoistedBinary::new(binary_name, binary_path);
registry.binaries.push(binary);
}
let toml = toml::to_string(®istry)?;
file.write_all(toml.as_bytes())?;
Ok(())
}
pub fn list() -> Result<()> {
HoistRegistry::setup()?;
let registry_file = HoistRegistry::path()?;
let mut file = std::fs::OpenOptions::new().read(true).open(registry_file)?;
let mut registry_toml = String::new();
file.read_to_string(&mut registry_toml)?;
let registry: HoistRegistry = toml::from_str(®istry_toml)?;
for binary in registry.binaries {
println!("{}", binary.name);
}
Ok(())
}
pub fn hoist(binaries: Option<Vec<String>>) -> Result<()> {
HoistRegistry::setup()?;
let registry_file = HoistRegistry::path()?;
let mut file = std::fs::OpenOptions::new().read(true).open(registry_file)?;
let mut registry_toml = String::new();
file.read_to_string(&mut registry_toml)?;
let registry: HoistRegistry = toml::from_str(®istry_toml)?;
let binaries = binaries.unwrap_or_default();
for binary in binaries {
let binary = registry
.binaries
.iter()
.find(|b| b.name == binary)
.ok_or_else(|| anyhow::anyhow!("Binary not found in hoist registry"))?;
let binary_path = binary.location.clone();
let binary_path = binary_path.canonicalize()?;
let bin_file_name = binary_path
.file_name()
.ok_or(anyhow::anyhow!("[std] failed to extract binary name"))?;
let binary_name = bin_file_name
.to_str()
.ok_or(anyhow::anyhow!(
"[std] failed to convert binary path name to string"
))?
.to_string();
let binary_path = binary_path
.to_str()
.ok_or(anyhow::anyhow!(
"[std] failed to convert binary path name to string"
))?
.to_string();
println!("export PATH={}:$PATH", binary_path);
println!("alias {}={}", binary_name, binary_name);
}
Ok(())
}
}
pub fn init_tracing_subscriber(verbosity_level: u8) -> Result<()> {
let subscriber = tracing_subscriber::fmt()
.with_max_level(match verbosity_level {
0 => Level::ERROR,
1 => Level::WARN,
2 => Level::INFO,
3 => Level::DEBUG,
_ => Level::TRACE,
})
.finish();
tracing::subscriber::set_global_default(subscriber).map_err(|e| anyhow::anyhow!(e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_setup() {
let tempdir = tempfile::tempdir().unwrap();
let test_tempdir = tempdir.path().join("test_install");
std::fs::create_dir(&test_tempdir).unwrap();
let bash_file = test_tempdir.join(".bashrc");
std::fs::File::create(&bash_file).unwrap();
let original_home = std::env::var_os("HOME").unwrap();
std::env::set_var("HOME", test_tempdir);
HoistRegistry::setup().unwrap();
let hoist_dir = HoistRegistry::dir().unwrap();
assert!(std::path::Path::new(&hoist_dir).exists());
let registry_file = HoistRegistry::path().unwrap();
assert!(std::path::Path::new(®istry_file).exists());
let mut file = std::fs::OpenOptions::new()
.read(true)
.open(registry_file)
.unwrap();
let mut registry_toml = String::new();
file.read_to_string(&mut registry_toml).unwrap();
let registry: HoistRegistry = toml::from_str(®istry_toml).unwrap();
assert_eq!(registry, HoistRegistry::default());
let hook_file = HoistRegistry::hook_identifier().unwrap();
assert!(std::path::Path::new(&hook_file).exists());
let mut file = std::fs::OpenOptions::new()
.read(true)
.open(bash_file)
.unwrap();
let mut bash_file_contents = String::new();
file.read_to_string(&mut bash_file_contents).unwrap();
assert_eq!(bash_file_contents, INSTALL_BASH_FUNCTION);
std::env::set_var("HOME", original_home);
}
}