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