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::cargo::cargo_bin_dir;
27use crate::installers::commitlint::Commitlint;
28use crate::installers::deno::Deno;
29use crate::installers::docker_langserver::DockerLangServer;
30use crate::installers::eslint_d::EslintD;
31use crate::installers::graphql_lsp::GraphQlLsp;
32use crate::installers::hadolint::Hadolint;
33use crate::installers::helm_ls::HelmLs;
34use crate::installers::lua_ls::LuaLanguageServer;
35use crate::installers::marksman::Marksman;
36use crate::installers::nvim::Nvim;
37use crate::installers::opencode::Opencode;
38use crate::installers::prettierd::PrettierD;
39use crate::installers::quicktype::Quicktype;
40use crate::installers::rio::Rio;
41use crate::installers::ruff_lsp::RuffLsp;
42use crate::installers::shellcheck::Shellcheck;
43use crate::installers::sql_language_server::SqlLanguageServer;
44use crate::installers::sqruff::Sqruff;
45use crate::installers::starship::Starship;
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    let dev_tools_path = Path::new(dev_tools_dir);
85    let bin_path = Path::new(bin_dir);
86    std::fs::create_dir_all(dev_tools_path)?;
87    std::fs::create_dir_all(bin_path)?;
88
89    let cargo_bin_dir = cargo_bin_dir()?;
90    let parallel_installers = parallel_installers(dev_tools_path, bin_path, &sys_info);
91    let managed_cargo_installers = managed_cargo_installers(&cargo_bin_dir, bin_path);
92    let (selected_parallel_installers, selected_cargo_installers, unknown_bin_names) =
93        select_installers(&supplied_bin_names, &parallel_installers, &managed_cargo_installers);
94
95    if !unknown_bin_names.is_empty() {
96        eprintln!(
97            "{} bins without matching installers",
98            format!("{unknown_bin_names:#?}").yellow().bold()
99        );
100    }
101
102    let installers_res = run_installers(selected_parallel_installers, selected_cargo_installers);
103
104    if let Err(errors) = report(&installers_res) {
105        eprintln!(
106            "{} | errors_count={} bin_names={errors:#?}",
107            "error installing tools".red(),
108            errors.len()
109        );
110        std::process::exit(1);
111    }
112
113    ytil_sys::rm::rm_dead_symlinks(bin_dir)?;
114
115    Ok(())
116}
117
118/// Construct installers that can run independently.
119fn parallel_installers<'a>(
120    dev_tools_dir: &'a Path,
121    bin_dir: &'a Path,
122    sys_info: &'a SysInfo,
123) -> Vec<Box<dyn Installer + 'a>> {
124    vec![
125        Box::new(Alacritty { dev_tools_dir, bin_dir }),
126        Box::new(BashLanguageServer { dev_tools_dir, bin_dir }),
127        Box::new(Commitlint { dev_tools_dir, bin_dir }),
128        Box::new(Deno { bin_dir, sys_info }),
129        Box::new(DockerLangServer { dev_tools_dir, bin_dir }),
130        Box::new(EslintD { dev_tools_dir, bin_dir }),
131        Box::new(GraphQlLsp { dev_tools_dir, bin_dir }),
132        Box::new(Hadolint { bin_dir, sys_info }),
133        Box::new(HelmLs { bin_dir, sys_info }),
134        Box::new(LuaLanguageServer {
135            dev_tools_dir,
136            sys_info,
137        }),
138        Box::new(Marksman { bin_dir, sys_info }),
139        Box::new(Nvim { dev_tools_dir, bin_dir }),
140        Box::new(Opencode { bin_dir, sys_info }),
141        Box::new(PrettierD { dev_tools_dir, bin_dir }),
142        Box::new(Quicktype { dev_tools_dir, bin_dir }),
143        Box::new(Rio { dev_tools_dir, bin_dir }),
144        Box::new(RuffLsp { dev_tools_dir, bin_dir }),
145        Box::new(Shellcheck { bin_dir, sys_info }),
146        Box::new(Sqruff { bin_dir, sys_info }),
147        Box::new(SqlLanguageServer { dev_tools_dir, bin_dir }),
148        Box::new(Starship { dev_tools_dir, bin_dir }),
149        Box::new(TerraformLs { bin_dir, sys_info }),
150        Box::new(TypescriptLanguageServer { dev_tools_dir, bin_dir }),
151        Box::new(TyposLsp { bin_dir, sys_info }),
152        Box::new(VsCodeLangServers { dev_tools_dir, bin_dir }),
153        Box::new(YamlLanguageServer { dev_tools_dir, bin_dir }),
154        Box::new(Zellij { dev_tools_dir, bin_dir }),
155    ]
156}
157
158/// Construct the declared Cargo and Git tool inventory.
159fn managed_cargo_installers<'a>(cargo_bin_dir: &'a Path, bin_dir: &'a Path) -> Vec<Box<dyn Installer + 'a>> {
160    vec![
161        Box::new(Cargo::registry(
162            cargo_bin_dir,
163            bin_dir,
164            "cargo-auditable",
165            "cargo-auditable",
166        )),
167        Box::new(Cargo::registry_with_features(
168            cargo_bin_dir,
169            bin_dir,
170            "cargo-audit",
171            "cargo-audit",
172            "fix",
173        )),
174        Box::new(Cargo::registry(
175            cargo_bin_dir,
176            bin_dir,
177            "cargo-machete",
178            "cargo-machete",
179        )),
180        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "cargo-make", "cargo-make")),
181        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "cargo-sort", "cargo-sort")),
182        Box::new(Cargo::registry(
183            cargo_bin_dir,
184            bin_dir,
185            "cargo-sort-derives",
186            "cargo-sort-derives",
187        )),
188        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "ccase", "ccase")),
189        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "fd", "fd-find")),
190        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "jnv", "jnv")),
191        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "mise", "mise")),
192        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "pv", "pv")),
193        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "qj", "qj")),
194        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "rg", "ripgrep")),
195        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "sd", "sd")),
196        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "sqlx", "sqlx-cli")),
197        Box::new(Cargo::registry(
198            cargo_bin_dir,
199            bin_dir,
200            "tree-sitter",
201            "tree-sitter-cli",
202        )),
203        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "typos", "typos-cli")),
204        Box::new(Cargo::registry(cargo_bin_dir, bin_dir, "harper-ls", "harper-ls")),
205        Box::new(Cargo::registry_with_all_features(
206            cargo_bin_dir,
207            bin_dir,
208            "taplo",
209            "taplo-cli",
210        )),
211        Box::new(Cargo::nightly_git(
212            cargo_bin_dir,
213            bin_dir,
214            "rust-analyzer",
215            "https://github.com/rust-lang/rust-analyzer.git",
216            "master",
217        )),
218        Box::new(Cargo::locked_registry(
219            cargo_bin_dir,
220            bin_dir,
221            "cargo-nextest",
222            "cargo-nextest",
223        )),
224        Box::new(Cargo::registry(
225            cargo_bin_dir,
226            bin_dir,
227            "cargo-llvm-cov",
228            "cargo-llvm-cov",
229        )),
230        Box::new(Cargo::git(
231            cargo_bin_dir,
232            bin_dir,
233            "rtk",
234            "https://github.com/rtk-ai/rtk",
235        )),
236    ]
237}
238
239/// Select individual installers or expand the `cargo` selector group.
240fn select_installers<'installer, 'name>(
241    supplied_bin_names: &[&'name str],
242    parallel_installers: &'installer [Box<dyn Installer + 'installer>],
243    managed_cargo_installers: &'installer [Box<dyn Installer + 'installer>],
244) -> (
245    Vec<&'installer dyn Installer>,
246    Vec<&'installer dyn Installer>,
247    Vec<&'name str>,
248) {
249    if supplied_bin_names.is_empty() {
250        return (
251            parallel_installers.iter().map(Box::as_ref).collect(),
252            managed_cargo_installers.iter().map(Box::as_ref).collect(),
253            vec![],
254        );
255    }
256
257    let parallel_installer_map: HashMap<&str, &dyn Installer> = parallel_installers
258        .iter()
259        .map(|installer| (installer.bin_name(), installer.as_ref()))
260        .collect();
261    let cargo_installer_map: HashMap<&str, &dyn Installer> = managed_cargo_installers
262        .iter()
263        .map(|installer| (installer.bin_name(), installer.as_ref()))
264        .collect();
265
266    let mut selected_parallel_installers = Vec::with_capacity(supplied_bin_names.len());
267    let mut selected_cargo_installers = Vec::with_capacity(supplied_bin_names.len());
268    let mut unknown_installers = vec![];
269    let mut selected_bin_names = HashSet::new();
270    for chosen_bin in supplied_bin_names {
271        if *chosen_bin == "cargo" {
272            for installer in managed_cargo_installers {
273                if selected_bin_names.insert(installer.bin_name()) {
274                    selected_cargo_installers.push(installer.as_ref());
275                }
276            }
277        } else if let Some(&installer) = parallel_installer_map.get(chosen_bin) {
278            if selected_bin_names.insert(installer.bin_name()) {
279                selected_parallel_installers.push(installer);
280            }
281        } else if let Some(&installer) = cargo_installer_map.get(chosen_bin) {
282            if selected_bin_names.insert(installer.bin_name()) {
283                selected_cargo_installers.push(installer);
284            }
285        } else {
286            unknown_installers.push(*chosen_bin);
287        }
288    }
289
290    (
291        selected_parallel_installers,
292        selected_cargo_installers,
293        unknown_installers,
294    )
295}
296
297/// Run independent installers concurrently and Cargo installers on one serial worker.
298fn run_installers<'a>(
299    selected_parallel_installers: Vec<&'a dyn Installer>,
300    selected_cargo_installers: Vec<&'a dyn Installer>,
301) -> Vec<(&'a str, std::thread::Result<rootcause::Result<()>>)> {
302    std::thread::scope(|scope| {
303        let mut handles = Vec::with_capacity(selected_parallel_installers.len());
304        for installer in selected_parallel_installers {
305            handles.push((installer.bin_name(), scope.spawn(move || installer.run())));
306        }
307        let cargo_handle = scope.spawn(move || {
308            selected_cargo_installers
309                .into_iter()
310                .map(|installer| (installer.bin_name(), installer.run()))
311                .collect::<Vec<_>>()
312        });
313
314        let mut results = Vec::with_capacity(handles.len());
315        for (bin_name, handle) in handles {
316            results.push((bin_name, handle.join()));
317        }
318        match cargo_handle.join() {
319            Ok(cargo_results) => results.extend(
320                cargo_results
321                    .into_iter()
322                    .map(|(bin_name, result)| (bin_name, Ok(result))),
323            ),
324            Err(error) => results.push(("cargo", Err(error))),
325        }
326        results
327    })
328}
329
330/// Summarize installer thread outcomes; collect failing bin names.
331///
332/// # Errors
333/// Returns failing bin names; installers handle detailed error output.
334fn report<'a>(installers_res: &'a [(&'a str, std::thread::Result<rootcause::Result<()>>)]) -> Result<(), Vec<&'a str>> {
335    let mut errors_bins = vec![];
336
337    for (bin_name, result) in installers_res {
338        match result {
339            Err(err) => {
340                eprintln!(
341                    "{} installer thread panicked error={}",
342                    bin_name.red(), // removed bold
343                    format!("{err:#?}").red()
344                );
345                errors_bins.push(*bin_name);
346            }
347            Ok(Err(_)) => errors_bins.push(bin_name),
348            Ok(Ok(())) => {}
349        }
350    }
351
352    if errors_bins.is_empty() {
353        return Ok(());
354    }
355    Err(errors_bins)
356}