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