1use std::path::Path;
2use std::path::PathBuf;
3use std::process::Command;
4use std::time::Duration;
5use std::time::Instant;
6
7use owo_colors::OwoColorize;
8use rootcause::bail;
9use rootcause::prelude::ResultExt;
10use ytil_cmd::Cmd;
11use ytil_cmd::CmdError;
12use ytil_cmd::CmdExt;
13
14pub mod alacritty;
15pub mod bash_language_server;
16pub mod cargo;
17pub mod commitlint;
18pub mod deno;
19pub mod docker_langserver;
20pub mod eslint_d;
21pub mod graphql_lsp;
22pub mod hadolint;
23pub mod helm_ls;
24pub mod lua_ls;
25pub mod marksman;
26pub mod nvim;
27pub mod opencode;
28pub mod prettierd;
29pub mod quicktype;
30pub mod rio;
31pub mod ruff_lsp;
32pub mod shellcheck;
33pub mod sql_language_server;
34pub mod sqruff;
35pub mod starship;
36pub mod terraform_ls;
37pub mod typescript_language_server;
38pub mod typos_lsp;
39pub mod vscode_langservers;
40pub mod yaml_language_server;
41pub mod zellij;
42
43pub trait Installer: Sync + Send {
45 fn bin_name(&self) -> &'static str;
47
48 fn install(&self) -> rootcause::Result<()>;
50
51 fn health_check(&self) -> Option<rootcause::Result<String>> {
53 let args = self.health_check_args()?;
54 let mut cmd = Command::new(self.bin_name());
55 cmd.args(args);
56
57 Some(run_health_check(cmd))
58 }
59
60 fn run(&self) -> rootcause::Result<()> {
65 let start = Instant::now();
66
67 self.install().inspect_err(|err| {
69 eprintln!(
70 "{} error installing\n{}",
71 self.bin_name().red().bold(),
72 format!("{err:#?}").red()
73 );
74 })?;
75
76 let past_install = Instant::now();
77
78 let mut health_check_duration = None;
80 let health_check_start = Instant::now();
81 let health_check_res = self.health_check();
82 if health_check_res.is_some() {
83 health_check_duration = Some(health_check_start.elapsed());
84 }
85
86 match health_check_res {
87 Some(Ok(health_check_output)) => {
88 let styled_bin_name = if self.should_verify_checksum() {
89 self.bin_name().green().bold().to_string()
90 } else {
91 self.bin_name().yellow().bold().to_string()
92 };
93 println!(
94 "{styled_bin_name} {} health_check_output=\n{}",
95 format_timing(start, past_install, health_check_duration),
96 health_check_output.trim_matches(|c| c == '\n' || c == '\r')
97 );
98 }
99 Some(Err(err)) => {
100 eprintln!(
101 "{} error in health check {}\n{}",
102 self.bin_name().red(),
103 format_timing(start, past_install, health_check_duration),
104 format!("{err:#?}").red()
105 );
106 return Err(err);
107 }
108 None => {
109 let styled_bin_name = if self.should_verify_checksum() {
110 self.bin_name().blue().bold().to_string()
111 } else {
112 self.bin_name().magenta().bold().to_string()
113 };
114 println!(
115 "{styled_bin_name} {}",
116 format_timing(start, past_install, health_check_duration),
117 );
118 }
119 }
120
121 Ok(())
122 }
123
124 fn health_check_args(&self) -> Option<&[&str]> {
126 Some(&["--version"])
127 }
128
129 fn should_verify_checksum(&self) -> bool {
133 true
134 }
135}
136
137pub fn run_health_check(mut cmd: Command) -> rootcause::Result<String> {
139 let output = cmd.exec()?;
140 let output = std::str::from_utf8(&output.stdout).map_err(|err| CmdError::Utf8 {
141 cmd: Cmd::from(&cmd),
142 source: err,
143 })?;
144 Ok(output.to_owned())
145}
146
147pub trait SystemDependent {
148 fn target_arch_and_os(&self) -> (&str, &str);
149}
150
151pub fn install_npm_tool(
158 dev_tools_dir: &Path,
159 bin_dir: &Path,
160 bin_name: &str,
161 npm_name: &str,
162 packages: &[&str],
163) -> rootcause::Result<()> {
164 let target_dir = crate::downloaders::npm::run(dev_tools_dir, npm_name, packages)?;
165 let target = target_dir.join(bin_name);
166 ytil_sys::file::ln_sf(&target, &bin_dir.join(bin_name))?;
167 ytil_sys::file::chmod_x(target)?;
168 Ok(())
169}
170
171pub fn install_macos_app(app: &Path, bin_dir: &Path, bin_name: &str) -> rootcause::Result<()> {
182 let binary = app.join("Contents").join("MacOS").join(bin_name);
183
184 ytil_sys::file::ln_sf(&binary, &bin_dir.join(bin_name))?;
185 ytil_sys::file::chmod_x(&binary)?;
186
187 let Some(app_filename) = app.file_name().and_then(|n| n.to_str()) else {
188 bail!("app path has no valid UTF-8 file name: {}", app.display());
189 };
190
191 let applications_app = PathBuf::from(format!("/Applications/{app_filename}"));
192 let applications_app_old = PathBuf::from(format!("/Applications/{app_filename}.old"));
193
194 if applications_app_old.exists() {
195 std::fs::remove_dir_all(&applications_app_old)
196 .context("error removing old .app backup")
197 .attach_with(|| format!("path={}", applications_app_old.display()))?;
198 }
199
200 if applications_app.is_symlink() {
201 std::fs::remove_file(&applications_app)
202 .context("error removing .app symlink")
203 .attach_with(|| format!("path={}", applications_app.display()))?;
204 } else if applications_app.exists() {
205 std::fs::rename(&applications_app, &applications_app_old)
206 .context("error renaming existing .app to .old")
207 .attach_with(|| format!("from={}", applications_app.display()))
208 .attach_with(|| format!("to={}", applications_app_old.display()))?;
209 }
210
211 ytil_cmd::silent_cmd("cp")
212 .args(["-R", &app.display().to_string(), "/Applications/"])
213 .status()
214 .context("failed to spawn cp for .app bundle")?
215 .exit_ok()
216 .context("cp .app bundle to /Applications failed")
217 .attach_with(|| format!("app={}", app.display()))?;
218
219 if applications_app_old.exists() {
220 std::fs::remove_dir_all(&applications_app_old)
221 .context("error cleaning up old .app backup")
222 .attach_with(|| format!("path={}", applications_app_old.display()))?;
223 }
224
225 Ok(())
226}
227
228fn format_timing(start: Instant, past_install: Instant, health_check: Option<Duration>) -> String {
230 format!(
231 "install_time={:?} health_check_time={:?} total_time={:?}",
232 past_install.duration_since(start),
233 health_check,
234 start.elapsed()
235 )
236}