From d01d9b8469ca99c1e62b8963cce1b2c2bf6cd6b3 Mon Sep 17 00:00:00 2001 From: Santiago Carmuega Date: Sun, 20 Apr 2025 23:15:11 -0300 Subject: [PATCH] feat: improve install experience --- Cargo.lock | 21 +++++++++++ Cargo.toml | 1 + src/cmds/default.rs | 4 +- src/cmds/install.rs | 61 ++++++++++++++++++++----------- src/cmds/mod.rs | 1 + src/cmds/show.rs | 48 ++++++++++++++++++++++++ src/main.rs | 3 ++ src/perm_path.rs | 89 +++++++++++++++++++++++++++++++-------------- src/tools.rs | 16 +++++++- 9 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 src/cmds/show.rs diff --git a/Cargo.lock b/Cargo.lock index 43250a0..e121a32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1001,6 +1001,17 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1995,6 +2006,7 @@ dependencies = [ "tar", "thiserror 2.0.12", "tokio", + "xz2", ] [[package]] @@ -2409,6 +2421,15 @@ dependencies = [ "rustix", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 1149d9a..6278519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ reqwest = { version = "0.11", features = ["json"] } futures-util = "0.3" flate2 = "1.0" tar = "0.4" +xz2 = "0.1.7" # The profile that 'cargo dist' will build with [profile.dist] diff --git a/src/cmds/default.rs b/src/cmds/default.rs index 74cdae2..d00954d 100644 --- a/src/cmds/default.rs +++ b/src/cmds/default.rs @@ -8,7 +8,7 @@ use crate::{Config, perm_path}; #[derive(Parser, Default)] pub struct Args { - #[arg(short, long, default_value = "stable")] + #[arg(default_value = "stable")] pub channel: String, } @@ -31,7 +31,7 @@ pub async fn run(args: &Args, config: &Config) -> anyhow::Result<()> { set_default_channel(&config.root_dir(), &args.channel)?; println!("Set default channel to {}", args.channel); - println!("Checking or updating PATH variable"); + println!("updating PATH variable"); perm_path::check_or_update(config)?; Ok(()) } diff --git a/src/cmds/install.rs b/src/cmds/install.rs index e4778ab..ac2ec0c 100644 --- a/src/cmds/install.rs +++ b/src/cmds/install.rs @@ -10,6 +10,8 @@ use std::fs; use std::io::Write; use std::path::PathBuf; use tar::Archive; +use tar::Entry; +use xz2::read::XzDecoder; use crate::{Config, tools::*}; @@ -44,15 +46,14 @@ pub async fn download_binary(url: &str, path: &PathBuf) -> Result<()> { Ok(()) } -fn extract_binary(path: &PathBuf, install_dir: &PathBuf, tool_name: &str) -> Result<()> { - let tar_gz = fs::File::open(path)?; - let tar = GzDecoder::new(tar_gz); - let mut archive = Archive::new(tar); - - // Find the first file that starts with the tool name - let mut binary_entry = None; +// Find the first file that starts with the tool name +fn extract_binary_main_entry( + mut archive: Archive, + install_dir: &PathBuf, + tool_name: &str, +) -> anyhow::Result<()> { for entry in archive.entries()? { - let entry = entry?; + let mut entry = entry?; let path = entry.path()?; let filename = path .file_name() @@ -60,23 +61,39 @@ fn extract_binary(path: &PathBuf, install_dir: &PathBuf, tool_name: &str) -> Res .context("Invalid filename in archive")?; if filename.starts_with(tool_name) && entry.header().entry_type().is_file() { - binary_entry = Some(entry); - break; + let binary_path = install_dir.join(filename); + entry.unpack(&binary_path)?; + + // Make the binary executable + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&binary_path)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&binary_path, perms)?; + } } } - let mut binary_entry = binary_entry.context("No matching binary found in archive")?; - let binary_path = install_dir.join(tool_name); - binary_entry.unpack(&binary_path)?; - - // Make the binary executable - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(&binary_path)?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(&binary_path, perms)?; - } + anyhow::bail!("No matching binary found in archive") +} + +fn extract_binary(path: &PathBuf, install_dir: &PathBuf, tool_name: &str) -> Result<()> { + let file = fs::File::open(path)?; + + let extension = path.extension().and_then(|s| s.to_str()); + + match extension { + Some("gz") => { + let archive = Archive::new(GzDecoder::new(file)); + extract_binary_main_entry(archive, install_dir, tool_name) + } + Some("xz") => { + let archive = Archive::new(XzDecoder::new(file)); + extract_binary_main_entry(archive, install_dir, tool_name) + } + _ => anyhow::bail!("Unsupported archive format. Expected .gz or .xz"), + }; Ok(()) } diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index 902b84e..03ca510 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -1,2 +1,3 @@ pub mod default; pub mod install; +pub mod show; diff --git a/src/cmds/show.rs b/src/cmds/show.rs new file mode 100644 index 0000000..ae2660f --- /dev/null +++ b/src/cmds/show.rs @@ -0,0 +1,48 @@ +use std::process::Command; + +use crate::Config; + +#[derive(Debug, clap::Parser)] +pub struct Args { + pub tool: Option, +} + +fn print_tool(tool: &crate::tools::Tool, config: &Config) -> anyhow::Result<()> { + println!("bin path: {}", tool.bin_path(config).display()); + + println!( + "github repo: https://github.com/{}/{}", + tool.repo_owner, tool.repo_name + ); + + let version = Command::new(tool.bin_path(config)) + .arg("--version") + .output()?; + + let version = String::from_utf8(version.stdout)?; + + let version = if version.is_empty() { + "not reported\n".to_string() + } else { + version + }; + + println!("version: {}", version); + + Ok(()) +} + +pub async fn run(args: &Args, config: &Config) -> anyhow::Result<()> { + // for each tool, trigger a shell command to print the version + for tool in crate::tools::all_tools() { + println!("{}", tool.name); + + let ok = print_tool(&tool, config); + + if let Err(e) = ok { + println!("error: {}", e); + } + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 9d9316f..30f05a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,8 @@ enum Commands { Uninstall, /// Set the default channel Default(cmds::default::Args), + /// Show the version of the tx3 toolchain + Show(cmds::show::Args), } pub struct Config { @@ -85,6 +87,7 @@ async fn main() -> Result<()> { Commands::Update => todo!(), Commands::Uninstall => todo!(), Commands::Default(args) => cmds::default::run(&args, &config).await?, + Commands::Show(args) => cmds::show::run(&args, &config).await?, } } else { cmds::install::run(&cmds::install::Args::default(), &config).await?; diff --git a/src/perm_path.rs b/src/perm_path.rs index 48b2a4c..423ef12 100644 --- a/src/perm_path.rs +++ b/src/perm_path.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use dirs; use std::env; use std::fmt; @@ -7,16 +8,42 @@ use std::path::{Path, PathBuf}; use crate::Config; -/// Common profile configuration file names in order of preference -const PROFILE_FILES: &[&str] = &[ - //".bash_profile", - //".bash_login", - //".profile", - ".zshrc", - //".zprofile", -]; +enum KnownShell { + Posix, + Bash, + Zsh, +} + +impl KnownShell { + fn rc_files(&self) -> Vec<&str> { + match self { + KnownShell::Posix => vec![".profile"], + KnownShell::Bash => vec![".bash_profile", ".bash_login", ".bashrc"], + KnownShell::Zsh => vec![".zshrc"], + } + } +} + +fn known_shells() -> Vec { + vec![KnownShell::Posix, KnownShell::Bash, KnownShell::Zsh] +} + +fn source_cmd(root_dir: &Path) -> String { + format!( + r#" +export TX3_ROOT="{}" +export PATH="$TX3_ROOT/default/bin:$PATH" +"#, + root_dir.to_str().unwrap() + ) +} + +fn file_contains(profile_path: &Path, source_cmd: &str) -> bool { + let contents = std::fs::read_to_string(profile_path).unwrap(); + contents.contains(source_cmd) +} -pub fn append_file(profile_path: PathBuf, home_dir: PathBuf) -> io::Result<()> { +fn append_file(profile_path: &Path, source_cmd: &str) -> anyhow::Result<()> { println!( "Appending to profile file: {}", profile_path.to_str().unwrap() @@ -27,18 +54,34 @@ pub fn append_file(profile_path: PathBuf, home_dir: PathBuf) -> io::Result<()> { .create(false) .open(profile_path)?; - writeln!(profile, "\nexport TX3_ROOT={}", home_dir.to_str().unwrap())?; - writeln!(profile, "export PATH=\"$TX3_ROOT/default/bin:$PATH\"")?; - profile.flush() + write!(profile, "{}", source_cmd)?; + profile.flush()?; + + Ok(()) } -#[cfg(target_family = "unix")] fn update_all_profiles(config: &Config) -> anyhow::Result<()> { - for profile_file in PROFILE_FILES { - let profile_path = dirs::home_dir().unwrap().join(profile_file); + for sh in known_shells() { + let source_cmd = source_cmd(&config.root_dir()); + + for rc in sh.rc_files() { + let profile_path = dirs::home_dir() + .context("can't find user's home dir")? + .join(rc); + + if !profile_path.exists() { + continue; + } + + if file_contains(&profile_path, &source_cmd) { + println!( + "{} already contains the source command", + profile_path.to_str().unwrap() + ); + continue; + } - if Path::new(&profile_path).exists() { - append_file(profile_path, config.root_dir())?; + append_file(&profile_path, &source_cmd)?; } } @@ -46,20 +89,10 @@ fn update_all_profiles(config: &Config) -> anyhow::Result<()> { } pub fn check_or_update(config: &Config) -> anyhow::Result<()> { - let path = env::var("PATH").unwrap(); - - if path.contains(".tx3/default/bin") { - println!("Tx3 toolchain already in $PATH"); - return Ok(()); - } - update_all_profiles(config)?; println!("\nRestart your shell or run:"); - println!( - "export PATH=\"{}/default/bin:$PATH\"", - config.root_dir().display() - ); + println!("{}", source_cmd(&config.root_dir())); Ok(()) } diff --git a/src/tools.rs b/src/tools.rs index c47985c..3a0e78b 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -6,7 +6,19 @@ pub struct Tool { pub repo_name: String, } -use std::sync::OnceLock; +impl Tool { + pub fn bin_path(&self, config: &Config) -> PathBuf { + config.bin_dir().join(self.name.clone()) + } + + pub fn version_cmd(&self) -> String { + format!("{} --version", self.name) + } +} + +use std::{path::PathBuf, sync::OnceLock}; + +use crate::Config; static TOOLS: OnceLock> = OnceLock::new(); @@ -22,7 +34,7 @@ pub fn all_tools() -> impl Iterator { repo_name: "trix".to_string(), }, Tool { - name: "lsp".to_string(), + name: "tx3-lsp".to_string(), description: "A language server for tx3".to_string(), min_version: "0.1.0".to_string(), repo_owner: "tx3-lang".to_string(),