Skip to main content

vtcode_core/tools/ripgrep_installer/
mod.rs

1//! Ripgrep availability detection and explicit installation management.
2//!
3//! This module handles detecting if ripgrep is available and installing it on
4//! demand through `vtcode dependencies install ripgrep`.
5
6mod platform;
7mod state;
8
9use crate::tools::install_support::run_version_probe;
10use crate::tools::ripgrep_binary::RIPGREP_INSTALL_COMMAND;
11use anyhow::{Result, anyhow};
12use std::process::Command;
13
14use self::platform::install_with_smart_detection;
15use self::state::{InstallLockGuard, InstallationCache};
16
17/// Result of ripgrep availability check
18#[derive(Debug, Clone, PartialEq)]
19pub enum RipgrepStatus {
20    /// Ripgrep is available and working
21    Available { version: String },
22    /// Ripgrep is not installed
23    NotFound,
24    /// Ripgrep exists but returned an error
25    Error { reason: String },
26}
27
28impl RipgrepStatus {
29    /// Check if ripgrep is currently available
30    pub fn check() -> Self {
31        debug_log("Checking ripgrep availability...");
32        let mut command = Command::new("rg");
33        command.arg("--version");
34        match run_version_probe(command) {
35            Ok(output) => {
36                if output.status.success() {
37                    let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
38                    if version.is_empty() {
39                        debug_log("ripgrep found but returned empty version");
40                        RipgrepStatus::Error {
41                            reason: "rg --version returned empty output".to_string(),
42                        }
43                    } else {
44                        debug_log(&format!("ripgrep available: {version}"));
45                        RipgrepStatus::Available { version }
46                    }
47                } else {
48                    let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
49                    debug_log(&format!("ripgrep check failed: {stderr}"));
50                    RipgrepStatus::Error {
51                        reason: if stderr.is_empty() {
52                            "rg exited with error".to_string()
53                        } else {
54                            stderr
55                        },
56                    }
57                }
58            }
59            Err(err) => {
60                if err.kind() == std::io::ErrorKind::NotFound {
61                    debug_log("ripgrep not found in PATH");
62                    RipgrepStatus::NotFound
63                } else {
64                    debug_log(&format!("ripgrep check error: {err}"));
65                    RipgrepStatus::Error { reason: err.to_string() }
66                }
67            }
68        }
69    }
70
71    /// Attempt to install ripgrep for the current platform.
72    /// Uses smart installer detection to try available tools first.
73    pub fn install() -> Result<()> {
74        debug_log("Installation attempt started");
75
76        if matches!(Self::check(), RipgrepStatus::Available { .. }) {
77            debug_log("ripgrep already available; skipping installation");
78            return Ok(());
79        }
80
81        if std::env::var("VTCODE_RIPGREP_NO_INSTALL").is_ok() {
82            debug_log("Auto-install disabled via VTCODE_RIPGREP_NO_INSTALL");
83            return Err(anyhow!("Ripgrep auto-installation disabled via VTCODE_RIPGREP_NO_INSTALL"));
84        }
85
86        if InstallLockGuard::is_install_in_progress() {
87            debug_log("Another installation is already in progress, skipping");
88            return Err(anyhow!("Ripgrep installation already in progress"));
89        }
90
91        let _lock = InstallLockGuard::acquire()?;
92        debug_log("Installation lock acquired");
93
94        if !InstallationCache::is_stale()
95            && let Ok(cache) = InstallationCache::load()
96            && cache.status == "failed"
97        {
98            let reason = cache.failure_reason.as_deref().unwrap_or("unknown reason");
99            debug_log(&format!("Cache shows previous failure: {reason}"));
100            return Err(anyhow!("Previous installation attempt failed ({reason}). Not retrying for 24 hours."));
101        }
102
103        let result = install_with_smart_detection();
104
105        match result {
106            Ok(()) => match Self::check() {
107                RipgrepStatus::Available { .. } => {
108                    debug_log("Installation verified successfully");
109                    InstallationCache::mark_success("auto-detected");
110                    Ok(())
111                }
112                status => {
113                    let msg = format!("Installation verification failed: {status:?}");
114                    debug_log(&msg);
115                    InstallationCache::mark_failed("auto-detected", &msg);
116                    Err(anyhow!(msg))
117                }
118            },
119            Err(err) => {
120                let msg = err.to_string();
121                debug_log(&format!("Installation failed: {msg}"));
122                InstallationCache::mark_failed("all-methods", &msg);
123                Err(anyhow!(msg))
124            }
125        }
126    }
127
128    /// Check if ripgrep is available, installing it if the caller explicitly requests that flow.
129    pub fn ensure_available() -> Result<Self> {
130        match Self::check() {
131            status @ RipgrepStatus::Available { .. } => Ok(status),
132            RipgrepStatus::NotFound => {
133                tracing::warn!("ripgrep not found, run `{RIPGREP_INSTALL_COMMAND}` to install");
134                Self::install()?;
135                Ok(Self::check())
136            }
137            status => Ok(status),
138        }
139    }
140}
141
142pub(super) fn debug_log(message: &str) {
143    if std::env::var("VTCODE_DEBUG_RIPGREP").is_ok() {
144        tracing::debug!(message, "ripgrep");
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_ripgrep_status_check() {
154        let status = RipgrepStatus::check();
155        tracing::debug!(?status, "Ripgrep status");
156    }
157}