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