diff --git a/Cargo.lock b/Cargo.lock index 4a20d35..ecabf7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,6 +210,7 @@ dependencies = [ "libc", "log", "lzma-sys", + "nondestructive", "octocrab", "openssl", "platforms", @@ -223,6 +224,7 @@ dependencies = [ "shellexpand", "tabled", "tar", + "textwrap", "tokio", "url", "validator", @@ -242,6 +244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", + "regex-automata", "serde", ] @@ -257,6 +260,12 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.8.0" @@ -1160,6 +1169,70 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lexical-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" +dependencies = [ + "lexical-util", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.164" @@ -1272,6 +1345,21 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nondestructive" +version = "0.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7431b02828e98a4366e3769b5e0dcfd922c51108c60c349d6acbb199f3d994" +dependencies = [ + "bstr", + "itoa", + "lexical-core", + "memchr", + "ryu", + "slab", + "twox-hash", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1500,6 +1588,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -1573,6 +1670,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.5.7" @@ -1961,6 +2088,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "snafu" version = "0.8.5" @@ -2004,6 +2137,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -2134,6 +2273,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2347,12 +2497,29 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "rand", + "static_assertions", +] + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-width" version = "0.1.11" @@ -2847,6 +3014,27 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "zerofrom" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 0952d8b..d82f97e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,11 +27,13 @@ easy-logging = "1" flate2 = "1.0" globset = "0.4.15" http = "1.1.0" +indoc = "2.0.5" is-terminal = "0.4.13" itertools = "0.13.0" octocrab = "0.42.0" libc = "0.2.164" log = "0.4.22" +nondestructive = "0.0.26" platforms = "3.5.0" regex = "1.11.1" reqwest = { version = "0.12.9", features = ["blocking"] } @@ -42,6 +44,7 @@ serde_yaml = "0.9.34" shellexpand = "3.1.0" tabled = { version = "0.16.0", features = ["ansi"] } tar = "0.4.43" +textwrap = "0.16.1" tokio = "1" url = "2.5.3" validator = { version = "0.19.0", features = ["derive"] } diff --git a/src/cli.rs b/src/cli.rs index eb919ff..c364411 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -165,6 +165,7 @@ pub fn parse_args() -> GenericResult { let mode = match command { "install" => Mode::Install { force: matches.get_flag("force"), + recheck_spec: false, }, "upgrade" => Mode::Upgrade, _ => unreachable!(), diff --git a/src/config.rs b/src/config.rs index 48b9b0c..a2192df 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,7 +21,7 @@ pub struct Config { source: Option, #[serde(rename = "path", default = "default_install_path", deserialize_with = "util::deserialize_path")] - pub install_path: PathBuf, + pub path: PathBuf, #[serde(default)] #[validate(nested)] @@ -53,7 +53,11 @@ impl Config { Ok(config) } - pub fn edit EmptyResult>(&mut self, edit: F) -> EmptyResult { + pub fn edit(&mut self, edit: E, process: P) -> EmptyResult + where + E: FnOnce(&mut Config, &mut Document) -> EmptyResult, + P: FnOnce(&Config) -> EmptyResult + { let error_prefix = "Failed to edit the configuration file: its current format is not supported by the underlaying library."; let mut expected_config = self.clone(); @@ -72,6 +76,12 @@ impl Config { return Err!("{error_prefix} Got the following unexpected config:\n{result}"); } + source.data = result.into_bytes(); + config.source.replace(source); + process(&config)?; + + let source = config.source.as_mut().unwrap(); + if !source.exists { if let Some(path) = source.path.parent() { fs::create_dir_all(path).map_err(|e| format!( @@ -80,15 +90,16 @@ impl Config { source.exists = true; } - source.data = result.into_bytes(); Config::write(&source.path, &source.data)?; - - config.source.replace(source); *self = config; Ok(()) } + pub fn get_tool_path(&self, name: &str, spec: &ToolSpec) -> PathBuf { + spec.path.as_ref().unwrap_or(&self.path).join(name) + } + pub fn update_tool(&mut self, raw: &mut Document, name: &str, spec: &ToolSpec) -> EmptyResult { let mut root = raw.as_mut().make_mapping(); diff --git a/src/install.rs b/src/install.rs index c277e19..a6db097 100644 --- a/src/install.rs +++ b/src/install.rs @@ -2,7 +2,7 @@ use std::fs::{self, OpenOptions}; use std::io::{self, Read}; use std::os::unix::fs::OpenOptionsExt; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, ExitCode}; use std::time::SystemTime; use easy_logging::GlobalContext; @@ -10,68 +10,104 @@ use log::{Level, debug, info, warn, error}; use semver::Version; use url::Url; -use crate::config::{Config, Tool}; -use crate::core::EmptyResult; +use crate::config::Config; +use crate::core::{EmptyResult, GenericResult}; use crate::download; -use crate::github::Github; +use crate::github::{self, Github}; use crate::matcher::Matcher; use crate::release::{self, Release}; +use crate::tool::ToolSpec; use crate::util; use crate::version::{self, ReleaseVersion}; #[derive(Clone, Copy)] pub enum Mode { - Install {force: bool}, + Install { + force: bool, + recheck_spec: bool, + }, Upgrade, } -pub fn install(config: &Config, mode: Mode, names: Option>) -> EmptyResult { - let tools: Vec<(&String, &Tool)> = match names { - Some(ref names) => { - let mut selected = Vec::new(); +pub fn install(config: &Config, mode: Mode, names: Vec) -> GenericResult { + let tools: Vec<(&String, &ToolSpec)> = if names.is_empty() { + config.tools.iter().collect() + } else { + let mut selected = Vec::new(); - for name in names { - let tool = config.tools.get(name).ok_or_else(|| format!( - "{name:?} tool is not specified in the configuration file"))?; - selected.push((name, tool)); - } + for name in &names { + let tool = config.tools.get(name).ok_or_else(|| format!( + "{name:?} tool is not specified in the configuration file"))?; + selected.push((name, tool)); + } - selected - }, - None => config.tools.iter().collect(), + selected }; - for (name, tool) in tools { + let github = Github::new(&config.github)?; + + for (name, spec) in tools { let _logging_context = GlobalContext::new_conditional(Level::Debug, name); - if names.is_none() { + if names.is_empty() { info!("Checking {name}..."); } - let path = tool.path.as_ref().unwrap_or(&config.path); - install_tool(config, name, tool, mode, path).map_err(|e| format!( + let install_path = config.get_tool_path(name, spec); + install_tool(name, spec, &github, mode, &install_path).map_err(|e| format!( "{name}: {e}"))?; } - Ok(()) + Ok(ExitCode::SUCCESS) +} + +pub fn install_spec(config: &mut Config, name: Option, spec: ToolSpec, force: bool) -> GenericResult { + let name = match name { + Some(name) => name, + None => github::parse_project_name(&spec.project)?.name, + }; + + let mut update_config = true; + + if let Some(registered) = config.tools.get(&name) { + if *registered == spec { + update_config = false + } else if !force && !util::confirm("The tool is already registered with different configuration. Override it?") { + return Ok(ExitCode::FAILURE); + } + } + + let github = Github::new(&config.github)?; + let install_path = config.get_tool_path(&name, &spec); + let install_mode = Mode::Install {force, recheck_spec: update_config}; + + if update_config { + config.edit( + |config, raw| config.update_tool(raw, &name, &spec), + |_| install_tool(&name, &spec, &github, install_mode, &install_path), + )?; + } else { + install_tool(&name, &spec, &github, install_mode, &install_path)?; + } + + Ok(ExitCode::SUCCESS) } -fn install_tool(config: &Config, name: &str, spec: &Tool, mut mode: Mode, path: &Path) -> EmptyResult { - let install_path = path.join(name); +fn install_tool(name: &str, spec: &ToolSpec, github: &Github, mut mode: Mode, install_path: &Path) -> EmptyResult { let tool = crate::tool::check(&install_path)?; match (mode, tool.is_some()) { - (Mode::Install{force: false}, true) => { + (Mode::Install{force: false, recheck_spec: false}, true) => { info!("{name} is already installed."); return Ok(()); }, (Mode::Upgrade, false) => { - mode = Mode::Install{force: false}; + mode = Mode::Install{force: false, recheck_spec: false}; } _ => {}, } - let release = Github::new(&config.github)?.get_release(&spec.project).map_err(|e| format!( + let release = github.get_release(&spec.project).map_err(|e| format!( "Failed to get latest release info for {}: {e}", spec.project))?; let release_version = &release.version; @@ -88,9 +124,9 @@ fn install_tool(config: &Config, name: &str, spec: &Tool, mut mode: Mode, path: version::get_binary_version(&install_path)); match mode { - Mode::Install {force: _} => if tool.is_none() { + Mode::Install {force, recheck_spec: _} => if tool.is_none() { info!("Installing {name}..."); - } else { + } else if force { match current_version { Some(current_version) => info!( "Reinstalling {name}: {current_version} -> {release_version}{changelog}", @@ -99,6 +135,9 @@ fn install_tool(config: &Config, name: &str, spec: &Tool, mut mode: Mode, path: None => info!("Reinstalling {name}..."), } + } else { + info!("{name} is already installed."); + return Ok(()); }, Mode::Upgrade => { diff --git a/src/list.rs b/src/list.rs index 1f11b8d..140c401 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,4 +1,6 @@ use std::io::Write; +use std::path::Path; +use std::process::ExitCode; use std::time::SystemTime; use ansi_term::Color; @@ -8,23 +10,24 @@ use tabled::{Table, Tabled}; use tabled::settings::{Alignment, Disable, Height, object::{Rows, Columns}, style::Style}; use crate::config::Config; -use crate::core::EmptyResult; +use crate::core::GenericResult; use crate::github::Github; use crate::tool::ToolSpec; use crate::version::{self, ReleaseVersion}; -pub fn list(config: &Config, full: bool) -> EmptyResult { +pub fn list(config: &Config, full: bool) -> GenericResult { if config.tools.is_empty() { - return Ok(()); + return Ok(ExitCode::SUCCESS); } let mut rows = Vec::new(); let github = Github::new(&config.github)?; let colored = std::io::stdout().is_terminal(); - for (name, tool) in &config.tools { + for (name, spec) in &config.tools { debug!("Checking {name}..."); - rows.push(list_tool(config, name, tool, &github, colored)); + let install_path = config.get_tool_path(name, spec); + rows.push(list_tool(name, spec, &github, &install_path, colored)); } let mut table = Table::new(&rows); @@ -39,7 +42,7 @@ pub fn list(config: &Config, full: bool) -> EmptyResult { } let _ = writeln!(std::io::stdout(), "{}", table); - Ok(()) + Ok(ExitCode::SUCCESS) } #[derive(Tabled)] @@ -57,9 +60,7 @@ struct ToolInfo { changelog: String, } -fn list_tool(config: &Config, name: &str, spec: &ToolSpec, github: &Github, colored: bool) -> ToolInfo { - let install_path = spec.path.as_ref().unwrap_or(&config.install_path).join(name); - +fn list_tool(name: &str, spec: &ToolSpec, github: &Github, install_path: &Path, colored: bool) -> ToolInfo { let tool = crate::tool::check(&install_path).unwrap_or_else(|e| { error!("{name}: {e}."); None diff --git a/src/main.rs b/src/main.rs index 619185c..d737bc5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,18 +13,18 @@ mod tool; mod util; mod version; +use core::GenericResult; use std::io::{self, Write}; use std::path::Path; -use std::process; +use std::process::{self, ExitCode}; use easy_logging::LoggingConfig; use log::error; use crate::cli::Action; use crate::config::Config; -use crate::core::EmptyResult; -fn main() { +fn main() -> ExitCode { let args = cli::parse_args().unwrap_or_else(|e| { let _ = writeln!(io::stderr(), "{}.", e); process::exit(1); @@ -35,20 +35,23 @@ fn main() { process::exit(1); } - if let Err(err) = run(&args.config_path, args.custom_config, args.action) { - let message = err.to_string(); + match run(&args.config_path, args.custom_config, args.action) { + Ok(code) => code, + Err(err) => { + let message = err.to_string(); - if message.contains('\n') || message.ends_with('.') { - error!("{message}"); - } else { - error!("{message}."); - } + if message.contains('\n') || message.ends_with('.') { + error!("{message}"); + } else { + error!("{message}."); + } - process::exit(1); + ExitCode::FAILURE + }, } } -fn run(config_path: &Path, custom_config: bool, action: Action) -> EmptyResult { +fn run(config_path: &Path, custom_config: bool, action: Action) -> GenericResult { let mut config = Config::load(config_path, custom_config).map_err(|e| format!( "Error while reading {:?} configuration file: {}", config_path, e))?;