idt/
installers.rs

1use std::process::Command;
2use std::time::Duration;
3use std::time::Instant;
4
5use color_eyre::owo_colors::OwoColorize as _;
6use ytil_cmd::Cmd;
7use ytil_cmd::CmdError;
8use ytil_cmd::CmdExt as _;
9
10pub mod bash_language_server;
11pub mod commitlint;
12pub mod deno;
13pub mod docker_langserver;
14pub mod eslint_d;
15pub mod graphql_lsp;
16pub mod hadolint;
17pub mod harper_ls;
18pub mod helm_ls;
19pub mod lua_ls;
20pub mod marksman;
21pub mod nvim;
22pub mod prettierd;
23pub mod quicktype;
24pub mod ruff_lsp;
25pub mod rust_analyzer;
26pub mod shellcheck;
27pub mod sql_language_server;
28pub mod sqruff;
29pub mod taplo;
30pub mod terraform_ls;
31pub mod typescript_language_server;
32pub mod typos_lsp;
33pub mod vscode_langservers;
34pub mod yaml_language_server;
35
36/// Trait for installing development tools and language servers.
37pub trait Installer: Sync + Send {
38    /// Returns the binary name of the tool to install.
39    fn bin_name(&self) -> &'static str;
40
41    /// Installs the tool to the configured location.
42    fn install(&self) -> color_eyre::Result<()>;
43
44    /// Checks if the tool is installed correctly.
45    fn check(&self) -> Option<color_eyre::Result<String>> {
46        let check_args = self.check_args()?;
47        let mut cmd = Command::new(self.bin_name());
48        cmd.args(check_args);
49
50        let check_res = cmd
51            .exec()
52            .and_then(|output| {
53                std::str::from_utf8(&output.stdout)
54                    .map(ToOwned::to_owned)
55                    .map_err(|err| CmdError::Utf8 {
56                        cmd: Cmd::from(&cmd),
57                        source: err,
58                    })
59            })
60            .map_err(From::from);
61
62        Some(check_res)
63    }
64
65    /// Execute install + optional check; emit status & per-phase timings.
66    ///
67    /// # Errors
68    /// - Any error from [`Installer::install`].
69    /// - Any process / UTF-8 error from the check phase.
70    ///
71    /// # Assumptions
72    /// - [`Installer::install`] leaves the binary runnable via [`Installer::bin_name`].
73    /// - [`Installer::check_args`] (when `Some`) is fast and exits 0 on success.
74    /// - ANSI color output acceptable (CI tolerates ANSI sequences).
75    ///
76    /// # Rationale
77    /// - Uniform UX: always attempt install then (if supported) lightweight smoke test.
78    /// - Prints a single line including phase durations: `install_time=<dur> check_time=<dur|None> total_time=<dur>` to
79    ///   quickly spot slow tools.
80    /// - Keeps tool-specific logic encapsulated; orchestration only formats and times phases.
81    ///
82    /// # Performance
83    /// - Overhead limited to a few [`Instant`] captures and formatted prints.
84    fn run(&self) -> color_eyre::Result<()> {
85        let start = Instant::now();
86
87        // Install phase
88        self.install().inspect_err(|err| {
89            eprintln!(
90                "{} error installing error=\n{}",
91                self.bin_name().red().bold(),
92                format!("{err:#?}").red()
93            );
94        })?;
95
96        let past_install = Instant::now();
97
98        // Check phase (optional)
99        let mut check_duration = None;
100        let check_start = Instant::now();
101        let check_res = self.check();
102        if check_res.is_some() {
103            check_duration = Some(check_start.elapsed());
104        }
105        match check_res {
106            Some(Ok(check_output)) => {
107                println!(
108                    "{} {} check_output=\n{}",
109                    self.bin_name().green().bold(),
110                    format_timing(start, past_install, check_duration),
111                    check_output.trim_matches(|c| c == '\n' || c == '\r')
112                );
113            }
114            Some(Err(err)) => {
115                eprintln!(
116                    "{} error checking {} error=\n{}",
117                    self.bin_name().red(),
118                    format_timing(start, past_install, check_duration),
119                    format!("{err:#?}").red()
120                );
121                return Err(err);
122            }
123            None => {
124                println!(
125                    "{} {}",
126                    self.bin_name().yellow().bold(),
127                    format_timing(start, past_install, check_duration),
128                );
129            }
130        }
131
132        Ok(())
133    }
134
135    /// Returns arguments for version check.
136    fn check_args(&self) -> Option<&[&str]> {
137        Some(&["--version"])
138    }
139}
140
141pub trait SystemDependent {
142    fn target_arch_and_os(&self) -> (&str, &str);
143}
144
145/// Format phase timing summary line.
146///
147/// # Rationale
148/// - Centralizes formatting logic to keep [`Installer::run`] concise and ensure consistent output shape.
149///
150/// # Performance
151/// - Negligible: a few duration subtractions and one allocation for formatting.
152fn format_timing(start: Instant, past_install: Instant, check: Option<Duration>) -> String {
153    format!(
154        "install_time={:?} check_time={:?} total_time={:?}",
155        past_install.duration_since(start),
156        check,
157        start.elapsed()
158    )
159}