Skip to main content

idt/
main.rs

1//! Install language servers, linters, formatters, and developer helpers concurrently.
2//!
3//! # Errors
4//! - Missing required argument (`dev_tools_dir` / `bin_dir`).
5//! - Directory creation fails.
6//! - GitHub authentication fails.
7//! - Installer thread panics.
8//! - Individual tool installation fails.
9//! - Dead symlink cleanup fails.
10#![feature(exit_status_error)]
11
12use std::collections::HashMap;
13use std::path::Path;
14
15use owo_colors::OwoColorize;
16use rootcause::prelude::ResultExt;
17use rootcause::report;
18use ytil_sys::SysInfo;
19use ytil_sys::cli::Args;
20
21use crate::installers::Installer;
22use crate::installers::alacritty::Alacritty;
23use crate::installers::bash_language_server::BashLanguageServer;
24use crate::installers::commitlint::Commitlint;
25use crate::installers::deno::Deno;
26use crate::installers::docker_langserver::DockerLangServer;
27use crate::installers::eslint_d::EslintD;
28use crate::installers::graphql_lsp::GraphQlLsp;
29use crate::installers::hadolint::Hadolint;
30use crate::installers::harper_ls::HarperLs;
31use crate::installers::helm_ls::HelmLs;
32use crate::installers::lua_ls::LuaLanguageServer;
33use crate::installers::marksman::Marksman;
34use crate::installers::nvim::Nvim;
35use crate::installers::opencode::Opencode;
36use crate::installers::prettierd::PrettierD;
37use crate::installers::quicktype::Quicktype;
38use crate::installers::rio::Rio;
39use crate::installers::ruff_lsp::RuffLsp;
40use crate::installers::rust_analyzer::RustAnalyzer;
41use crate::installers::shellcheck::Shellcheck;
42use crate::installers::sql_language_server::SqlLanguageServer;
43use crate::installers::sqruff::Sqruff;
44use crate::installers::starship::Starship;
45use crate::installers::taplo::Taplo;
46use crate::installers::terraform_ls::TerraformLs;
47use crate::installers::typescript_language_server::TypescriptLanguageServer;
48use crate::installers::typos_lsp::TyposLsp;
49use crate::installers::vscode_langservers::VsCodeLangServers;
50use crate::installers::yaml_language_server::YamlLanguageServer;
51use crate::installers::zellij::Zellij;
52
53mod downloaders;
54mod installers;
55
56/// Install language servers, linters, formatters, and developer helpers concurrently.
57#[ytil_sys::main]
58fn main() -> rootcause::Result<()> {
59    let args = ytil_sys::cli::get();
60    if args.has_help() {
61        println!("{}", include_str!("../help.txt"));
62        return Ok(());
63    }
64    println!(
65        "{:#?} started with args {}",
66        std::env::current_exe()?.bold().cyan(),
67        format!("{args:#?}").white().bold()
68    );
69
70    let dev_tools_dir = args
71        .first()
72        .ok_or_else(|| report!("missing dev_tools_dir arg"))
73        .attach_with(|| format!("args={args:#?}"))?
74        .trim_end_matches('/');
75    let bin_dir = args
76        .get(1)
77        .ok_or_else(|| report!("missing bin_dir arg"))
78        .attach_with(|| format!("args={args:#?}"))?
79        .trim_end_matches('/');
80    let supplied_bin_names: Vec<&str> = args.iter().skip(2).map(AsRef::as_ref).collect();
81
82    let sys_info = SysInfo::get()?;
83
84    std::fs::create_dir_all(dev_tools_dir)?;
85    std::fs::create_dir_all(bin_dir)?;
86
87    let all_installers: Vec<Box<dyn Installer>> = vec![
88        Box::new(Alacritty {
89            dev_tools_dir: Path::new(dev_tools_dir),
90            bin_dir: Path::new(bin_dir),
91        }),
92        Box::new(BashLanguageServer {
93            dev_tools_dir: Path::new(dev_tools_dir),
94            bin_dir: Path::new(bin_dir),
95        }),
96        Box::new(Commitlint {
97            dev_tools_dir: Path::new(dev_tools_dir),
98            bin_dir: Path::new(bin_dir),
99        }),
100        Box::new(Deno {
101            bin_dir: Path::new(bin_dir),
102            sys_info: &sys_info,
103        }),
104        Box::new(DockerLangServer {
105            dev_tools_dir: Path::new(dev_tools_dir),
106            bin_dir: Path::new(bin_dir),
107        }),
108        Box::new(EslintD {
109            dev_tools_dir: Path::new(dev_tools_dir),
110            bin_dir: Path::new(bin_dir),
111        }),
112        Box::new(GraphQlLsp {
113            dev_tools_dir: Path::new(dev_tools_dir),
114            bin_dir: Path::new(bin_dir),
115        }),
116        Box::new(Hadolint {
117            bin_dir: Path::new(bin_dir),
118            sys_info: &sys_info,
119        }),
120        Box::new(HarperLs {
121            bin_dir: Path::new(bin_dir),
122        }),
123        Box::new(HelmLs {
124            bin_dir: Path::new(bin_dir),
125            sys_info: &sys_info,
126        }),
127        Box::new(LuaLanguageServer {
128            dev_tools_dir: Path::new(dev_tools_dir),
129            sys_info: &sys_info,
130        }),
131        Box::new(Marksman {
132            bin_dir: Path::new(bin_dir),
133            sys_info: &sys_info,
134        }),
135        Box::new(Nvim {
136            dev_tools_dir: Path::new(dev_tools_dir),
137            bin_dir: Path::new(bin_dir),
138        }),
139        Box::new(Opencode {
140            bin_dir: Path::new(bin_dir),
141            sys_info: &sys_info,
142        }),
143        Box::new(PrettierD {
144            dev_tools_dir: Path::new(dev_tools_dir),
145            bin_dir: Path::new(bin_dir),
146        }),
147        Box::new(Quicktype {
148            dev_tools_dir: Path::new(dev_tools_dir),
149            bin_dir: Path::new(bin_dir),
150        }),
151        Box::new(Rio {
152            dev_tools_dir: Path::new(dev_tools_dir),
153            bin_dir: Path::new(bin_dir),
154        }),
155        Box::new(RuffLsp {
156            dev_tools_dir: Path::new(dev_tools_dir),
157            bin_dir: Path::new(bin_dir),
158        }),
159        Box::new(RustAnalyzer {
160            bin_dir: Path::new(bin_dir),
161            sys_info: &sys_info,
162        }),
163        Box::new(Shellcheck {
164            bin_dir: Path::new(bin_dir),
165            sys_info: &sys_info,
166        }),
167        Box::new(Sqruff {
168            bin_dir: Path::new(bin_dir),
169            sys_info: &sys_info,
170        }),
171        Box::new(SqlLanguageServer {
172            dev_tools_dir: Path::new(dev_tools_dir),
173            bin_dir: Path::new(bin_dir),
174        }),
175        Box::new(Starship {
176            dev_tools_dir: Path::new(dev_tools_dir),
177            bin_dir: Path::new(bin_dir),
178        }),
179        Box::new(Taplo {
180            bin_dir: Path::new(bin_dir),
181        }),
182        Box::new(TerraformLs {
183            bin_dir: Path::new(bin_dir),
184            sys_info: &sys_info,
185        }),
186        Box::new(TypescriptLanguageServer {
187            dev_tools_dir: Path::new(dev_tools_dir),
188            bin_dir: Path::new(bin_dir),
189        }),
190        Box::new(TyposLsp {
191            bin_dir: Path::new(bin_dir),
192            sys_info: &sys_info,
193        }),
194        Box::new(VsCodeLangServers {
195            dev_tools_dir: Path::new(dev_tools_dir),
196            bin_dir: Path::new(bin_dir),
197        }),
198        Box::new(YamlLanguageServer {
199            dev_tools_dir: Path::new(dev_tools_dir),
200            bin_dir: Path::new(bin_dir),
201        }),
202        Box::new(Zellij {
203            dev_tools_dir: Path::new(dev_tools_dir),
204            bin_dir: Path::new(bin_dir),
205        }),
206    ];
207
208    let (selected_installers, unknown_bin_names): (Vec<_>, Vec<_>) = if supplied_bin_names.is_empty() {
209        (all_installers.iter().collect(), vec![])
210    } else {
211        // Build HashMap for O(1) installer lookup instead of O(n) linear search
212        let installer_map: HashMap<&str, &Box<dyn Installer>> =
213            all_installers.iter().map(|i| (i.bin_name(), i)).collect();
214
215        let mut selected_installers = Vec::with_capacity(supplied_bin_names.len());
216        let mut unknown_installers = vec![];
217        for chosen_bin in supplied_bin_names {
218            if let Some(&installer) = installer_map.get(chosen_bin) {
219                selected_installers.push(installer);
220            } else {
221                unknown_installers.push(chosen_bin);
222            }
223        }
224        (selected_installers, unknown_installers)
225    };
226
227    if !unknown_bin_names.is_empty() {
228        eprintln!(
229            "{} bins without matching installers",
230            format!("{unknown_bin_names:#?}").yellow().bold()
231        );
232    }
233
234    let installers_res = std::thread::scope(|scope| {
235        let mut handles = Vec::with_capacity(selected_installers.len());
236        for installer in selected_installers {
237            handles.push((installer.bin_name(), scope.spawn(move || installer.run())));
238        }
239        let mut res = Vec::with_capacity(handles.len());
240        for (bin_name, handle) in handles {
241            res.push((bin_name, handle.join()));
242        }
243        res
244    });
245
246    if let Err(errors) = report(&installers_res) {
247        eprintln!(
248            "{} | errors_count={} bin_names={errors:#?}",
249            "error installing tools".red(),
250            errors.len()
251        );
252        std::process::exit(1);
253    }
254
255    ytil_sys::rm::rm_dead_symlinks(bin_dir)?;
256
257    Ok(())
258}
259
260/// Summarize installer thread outcomes; collect failing bin names.
261///
262/// # Errors
263/// Returns failing bin names; installers handle detailed error output.
264fn report<'a>(installers_res: &'a [(&'a str, std::thread::Result<rootcause::Result<()>>)]) -> Result<(), Vec<&'a str>> {
265    let mut errors_bins = vec![];
266
267    for (bin_name, result) in installers_res {
268        match result {
269            Err(err) => {
270                eprintln!(
271                    "{} installer thread panicked error={}",
272                    bin_name.red(), // removed bold
273                    format!("{err:#?}").red()
274                );
275                errors_bins.push(*bin_name);
276            }
277            Ok(Err(_)) => errors_bins.push(bin_name),
278            Ok(Ok(())) => {}
279        }
280    }
281
282    if errors_bins.is_empty() {
283        return Ok(());
284    }
285    Err(errors_bins)
286}