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::collections::HashSet;
14use std::path::Path;
15
16use owo_colors::OwoColorize;
17use rootcause::prelude::ResultExt;
18use rootcause::report;
19use ytil_sys::SysInfo;
20use ytil_sys::cli::Args;
21
22use crate::installers::Installer;
23use crate::installers::alacritty::Alacritty;
24use crate::installers::bash_language_server::BashLanguageServer;
25use crate::installers::cargo::Cargo;
26use crate::installers::commitlint::Commitlint;
27use crate::installers::deno::Deno;
28use crate::installers::docker_langserver::DockerLangServer;
29use crate::installers::eslint_d::EslintD;
30use crate::installers::graphql_lsp::GraphQlLsp;
31use crate::installers::hadolint::Hadolint;
32use crate::installers::helm_ls::HelmLs;
33use crate::installers::lua_ls::LuaLanguageServer;
34use crate::installers::marksman::Marksman;
35use crate::installers::nvim::Nvim;
36use crate::installers::opencode::Opencode;
37use crate::installers::prettierd::PrettierD;
38use crate::installers::quicktype::Quicktype;
39use crate::installers::rio::Rio;
40use crate::installers::ruff_lsp::RuffLsp;
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::terraform_ls::TerraformLs;
46use crate::installers::typescript_language_server::TypescriptLanguageServer;
47use crate::installers::typos_lsp::TyposLsp;
48use crate::installers::vscode_langservers::VsCodeLangServers;
49use crate::installers::yaml_language_server::YamlLanguageServer;
50use crate::installers::zellij::Zellij;
51
52mod downloaders;
53mod installers;
54
55/// Install language servers, linters, formatters, and developer helpers concurrently.
56#[ytil_sys::main]
57fn main() -> rootcause::Result<()> {
58    let args = ytil_sys::cli::get();
59    if args.has_help() {
60        println!(include_str!("../help.txt"));
61        return Ok(());
62    }
63    println!(
64        "{:#?} started with args {}",
65        std::env::current_exe()?.bold().cyan(),
66        format!("{args:#?}").white().bold()
67    );
68
69    let dev_tools_dir = args
70        .first()
71        .ok_or_else(|| {
72            eprintln!("{}", include_str!("../help.txt"));
73            report!("missing dev_tools_dir arg")
74        })
75        .attach_with(|| format!("args={args:#?}"))?
76        .trim_end_matches('/');
77    let bin_dir = args
78        .get(1)
79        .ok_or_else(|| {
80            eprintln!("{}", include_str!("../help.txt"));
81            report!("missing bin_dir arg")
82        })
83        .attach_with(|| format!("args={args:#?}"))?
84        .trim_end_matches('/');
85    let supplied_bin_names: Vec<&str> = args.iter().skip(2).map(AsRef::as_ref).collect();
86
87    let sys_info = SysInfo::get()?;
88
89    let dev_tools_path = Path::new(dev_tools_dir);
90    let bin_path = Path::new(bin_dir);
91    std::fs::create_dir_all(dev_tools_path)?;
92    std::fs::create_dir_all(bin_path)?;
93
94    let cargo_bin_dir = crate::installers::cargo::bin_dir()?;
95    let parallel_installers = parallel_installers(dev_tools_path, bin_path, &sys_info);
96    let managed_cargo_installers = managed_cargo_installers(&cargo_bin_dir, bin_path);
97    let (selected_parallel_installers, selected_cargo_installers, unknown_bin_names) =
98        select_installers(&supplied_bin_names, &parallel_installers, &managed_cargo_installers);
99
100    if !unknown_bin_names.is_empty() {
101        eprintln!(
102            "{} bins without matching installers",
103            format!("{unknown_bin_names:#?}").yellow().bold()
104        );
105    }
106
107    let installers_res = run_installers(selected_parallel_installers, selected_cargo_installers);
108
109    if let Err(errors) = report(&installers_res) {
110        eprintln!(
111            "{} | errors_count={} bin_names={errors:#?}",
112            "error installing tools".red(),
113            errors.len()
114        );
115        std::process::exit(1);
116    }
117
118    ytil_sys::rm::rm_dead_symlinks(bin_dir)?;
119
120    Ok(())
121}
122
123/// Construct installers that can run independently.
124fn parallel_installers<'a>(
125    dev_tools_dir: &'a Path,
126    bin_dir: &'a Path,
127    sys_info: &'a SysInfo,
128) -> Vec<Box<dyn Installer + 'a>> {
129    vec![
130        Box::new(Alacritty { dev_tools_dir, bin_dir }),
131        Box::new(BashLanguageServer { dev_tools_dir, bin_dir }),
132        Box::new(Commitlint { dev_tools_dir, bin_dir }),
133        Box::new(Deno { bin_dir, sys_info }),
134        Box::new(DockerLangServer { dev_tools_dir, bin_dir }),
135        Box::new(EslintD { dev_tools_dir, bin_dir }),
136        Box::new(GraphQlLsp { dev_tools_dir, bin_dir }),
137        Box::new(Hadolint { bin_dir, sys_info }),
138        Box::new(HelmLs { bin_dir, sys_info }),
139        Box::new(LuaLanguageServer {
140            dev_tools_dir,
141            sys_info,
142        }),
143        Box::new(Marksman { bin_dir, sys_info }),
144        Box::new(Nvim { dev_tools_dir, bin_dir }),
145        Box::new(Opencode { bin_dir, sys_info }),
146        Box::new(PrettierD { dev_tools_dir, bin_dir }),
147        Box::new(Quicktype { dev_tools_dir, bin_dir }),
148        Box::new(Rio { dev_tools_dir, bin_dir }),
149        Box::new(RuffLsp { dev_tools_dir, bin_dir }),
150        Box::new(Shellcheck { bin_dir, sys_info }),
151        Box::new(Sqruff { bin_dir, sys_info }),
152        Box::new(SqlLanguageServer { dev_tools_dir, bin_dir }),
153        Box::new(Starship { dev_tools_dir, bin_dir }),
154        Box::new(TerraformLs { bin_dir, sys_info }),
155        Box::new(TypescriptLanguageServer { dev_tools_dir, bin_dir }),
156        Box::new(TyposLsp { bin_dir, sys_info }),
157        Box::new(VsCodeLangServers { dev_tools_dir, bin_dir }),
158        Box::new(YamlLanguageServer { dev_tools_dir, bin_dir }),
159        Box::new(Zellij { dev_tools_dir, bin_dir }),
160    ]
161}
162
163/// Construct the declared Cargo and Git tool inventory.
164fn managed_cargo_installers<'a>(cargo_bin_dir: &'a Path, bin_dir: &'a Path) -> Vec<Box<dyn Installer + 'a>> {
165    vec![
166        Box::new(Cargo::registry_with_cargo_subcommand(
167            cargo_bin_dir,
168            bin_dir,
169            "cargo-auditable",
170            "cargo-auditable",
171            "auditable",
172        )),
173        Box::new(Cargo::registry_with_features(
174            cargo_bin_dir,
175            bin_dir,
176            "cargo-audit",
177            "cargo-audit",
178            "fix",
179        )),
180        Box::new(Cargo::registry_with_cargo_subcommand(
181            cargo_bin_dir,
182            bin_dir,
183            "cargo-diet",
184            "cargo-diet",
185            "diet",
186        )),
187        Box::new(Cargo::registry(
188            cargo_bin_dir,
189            bin_dir,
190            "cargo-machete",
191            "cargo-machete",
192        )),
193        Box::new(Cargo::registry_with_cargo_subcommand(
194            cargo_bin_dir,
195            bin_dir,
196            "cargo-make",
197            "cargo-make",
198            "make",
199        )),
200        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "cargo-sort", "cargo-sort")),
201        Box::new(Cargo::registry_with_cargo_subcommand(
202            cargo_bin_dir,
203            bin_dir,
204            "cargo-sort-derives",
205            "cargo-sort-derives",
206            "sort-derives",
207        )),
208        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "ccase", "ccase")),
209        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "fd", "fd-find")),
210        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "jnv", "jnv")),
211        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "mise", "mise")),
212        Box::new(Cargo::registry_with_health_check_args(
213            cargo_bin_dir,
214            bin_dir,
215            "pv",
216            "pv",
217            &["--help"],
218        )),
219        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "qj", "qj")),
220        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "rg", "ripgrep")),
221        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "sd", "sd")),
222        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "sqlx", "sqlx-cli")),
223        Box::new(Cargo::registry(
224            cargo_bin_dir,
225            bin_dir,
226            "tree-sitter",
227            "tree-sitter-cli",
228        )),
229        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "typos", "typos-cli")),
230        // Let's just use typos for now
231        // Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "harper-ls", "harper-ls")),
232        Box::new(Cargo::registry_with_all_features(
233            cargo_bin_dir,
234            bin_dir,
235            "taplo",
236            "taplo-cli",
237        )),
238        Box::new(Cargo::nightly_git(
239            cargo_bin_dir,
240            bin_dir,
241            "rust-analyzer",
242            "https://github.com/rust-lang/rust-analyzer.git",
243            "master",
244        )),
245        Box::new(Cargo::locked_registry(
246            cargo_bin_dir,
247            bin_dir,
248            "cargo-nextest",
249            "cargo-nextest",
250        )),
251        Box::new(Cargo::registry_with_cargo_subcommand(
252            cargo_bin_dir,
253            bin_dir,
254            "cargo-llvm-cov",
255            "cargo-llvm-cov",
256            "llvm-cov",
257        )),
258        Box::new(Cargo::git(
259            cargo_bin_dir,
260            bin_dir,
261            "rtk",
262            "https://github.com/rtk-ai/rtk",
263        )),
264        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "kache", "kache")),
265    ]
266}
267
268/// Select individual installers or expand the `cargo` selector group.
269fn select_installers<'installer, 'name>(
270    supplied_bin_names: &[&'name str],
271    parallel_installers: &'installer [Box<dyn Installer + 'installer>],
272    managed_cargo_installers: &'installer [Box<dyn Installer + 'installer>],
273) -> (
274    Vec<&'installer dyn Installer>,
275    Vec<&'installer dyn Installer>,
276    Vec<&'name str>,
277) {
278    if supplied_bin_names.is_empty() {
279        return (
280            parallel_installers.iter().map(Box::as_ref).collect(),
281            managed_cargo_installers.iter().map(Box::as_ref).collect(),
282            vec![],
283        );
284    }
285
286    let parallel_installer_map: HashMap<&str, &dyn Installer> = parallel_installers
287        .iter()
288        .map(|installer| (installer.bin_name(), installer.as_ref()))
289        .collect();
290    let cargo_installer_map: HashMap<&str, &dyn Installer> = managed_cargo_installers
291        .iter()
292        .map(|installer| (installer.bin_name(), installer.as_ref()))
293        .collect();
294
295    let mut selected_parallel_installers = Vec::with_capacity(supplied_bin_names.len());
296    let mut selected_cargo_installers = Vec::with_capacity(supplied_bin_names.len());
297    let mut unknown_installers = vec![];
298    let mut selected_bin_names = HashSet::new();
299    for chosen_bin in supplied_bin_names {
300        if *chosen_bin == "cargo" {
301            for installer in managed_cargo_installers {
302                if selected_bin_names.insert(installer.bin_name()) {
303                    selected_cargo_installers.push(installer.as_ref());
304                }
305            }
306        } else if let Some(&installer) = parallel_installer_map.get(chosen_bin) {
307            if selected_bin_names.insert(installer.bin_name()) {
308                selected_parallel_installers.push(installer);
309            }
310        } else if let Some(&installer) = cargo_installer_map.get(chosen_bin) {
311            if selected_bin_names.insert(installer.bin_name()) {
312                selected_cargo_installers.push(installer);
313            }
314        } else {
315            unknown_installers.push(*chosen_bin);
316        }
317    }
318
319    (
320        selected_parallel_installers,
321        selected_cargo_installers,
322        unknown_installers,
323    )
324}
325
326/// Run independent installers concurrently and Cargo installers on one serial worker.
327fn run_installers<'a>(
328    selected_parallel_installers: Vec<&'a dyn Installer>,
329    selected_cargo_installers: Vec<&'a dyn Installer>,
330) -> Vec<(&'a str, std::thread::Result<rootcause::Result<()>>)> {
331    std::thread::scope(|scope| {
332        let mut handles = Vec::with_capacity(selected_parallel_installers.len());
333        for installer in selected_parallel_installers {
334            handles.push((installer.bin_name(), scope.spawn(move || installer.run())));
335        }
336        let cargo_handle = scope.spawn(move || {
337            selected_cargo_installers
338                .into_iter()
339                .map(|installer| (installer.bin_name(), installer.run()))
340                .collect::<Vec<_>>()
341        });
342
343        let mut results = Vec::with_capacity(handles.len());
344        for (bin_name, handle) in handles {
345            results.push((bin_name, handle.join()));
346        }
347        match cargo_handle.join() {
348            Ok(cargo_results) => results.extend(
349                cargo_results
350                    .into_iter()
351                    .map(|(bin_name, result)| (bin_name, Ok(result))),
352            ),
353            Err(error) => results.push(("cargo", Err(error))),
354        }
355        results
356    })
357}
358
359/// Summarize installer thread outcomes; collect failing bin names.
360///
361/// # Errors
362/// Returns failing bin names; installers handle detailed error output.
363fn report<'a>(installers_res: &'a [(&'a str, std::thread::Result<rootcause::Result<()>>)]) -> Result<(), Vec<&'a str>> {
364    let mut errors_bins = vec![];
365
366    for (bin_name, result) in installers_res {
367        match result {
368            Err(err) => {
369                eprintln!(
370                    "{} installer thread panicked error={}",
371                    bin_name.red(), // removed bold
372                    format!("{err:#?}").red()
373                );
374                errors_bins.push(*bin_name);
375            }
376            Ok(Err(_)) => errors_bins.push(bin_name),
377            Ok(Ok(())) => {}
378        }
379    }
380
381    if errors_bins.is_empty() {
382        return Ok(());
383    }
384    Err(errors_bins)
385}