vtcode_core/tools/
ast_grep_binary.rs1use std::path::PathBuf;
2use std::sync::Mutex;
3
4use anyhow::Context;
5use once_cell::sync::Lazy;
6use vtcode_commons::VtCodePaths;
7
8pub const AST_GREP_BIN_ENV: &str = "VTCODE_AST_GREP_BIN";
9pub const AST_GREP_INSTALL_COMMAND: &str = "vtcode dependencies install ast-grep";
10
11pub(crate) static AST_GREP_OVERRIDE: Lazy<Mutex<AstGrepBinaryOverride>> =
12 Lazy::new(|| Mutex::new(AstGrepBinaryOverride::System));
13
14#[derive(Debug, Clone, Default)]
15pub(crate) enum AstGrepBinaryOverride {
16 #[default]
17 System,
18 Missing,
19 Path(PathBuf),
20}
21
22#[doc(hidden)]
23#[must_use]
24pub struct AstGrepBinaryOverrideGuard {
25 previous: AstGrepBinaryOverride,
26}
27
28impl Drop for AstGrepBinaryOverrideGuard {
29 fn drop(&mut self) {
30 if let Ok(mut guard) = AST_GREP_OVERRIDE.lock() {
31 *guard = self.previous.clone();
32 }
33 }
34}
35
36#[doc(hidden)]
37pub fn set_ast_grep_binary_override_for_tests(path: Option<PathBuf>) -> AstGrepBinaryOverrideGuard {
38 let mut state = AST_GREP_OVERRIDE.lock().unwrap_or_else(|e| e.into_inner());
39 let previous = state.clone();
40 *state = match path {
41 Some(path) => AstGrepBinaryOverride::Path(path),
42 None => AstGrepBinaryOverride::Missing,
43 };
44 AstGrepBinaryOverrideGuard { previous }
45}
46
47pub fn managed_ast_grep_bin_dir() -> anyhow::Result<PathBuf> {
48 VtCodePaths::resolve()
49 .map(|paths| paths.executable_dir().to_path_buf())
50 .context("could not resolve VT Code executable directory")
51}
52
53pub fn managed_ast_grep_binary_path() -> anyhow::Result<PathBuf> {
54 Ok(managed_ast_grep_bin_dir()?.join(canonical_ast_grep_binary_name()))
55}
56
57pub fn managed_ast_grep_alias_path() -> anyhow::Result<Option<PathBuf>> {
58 alias_ast_grep_binary_name()
59 .map(|name| managed_ast_grep_bin_dir().map(|directory| directory.join(name)))
60 .transpose()
61}
62
63pub fn managed_ast_grep_candidates() -> anyhow::Result<Vec<PathBuf>> {
64 let mut candidates = vec![managed_ast_grep_binary_path()?];
65 if let Some(alias) = managed_ast_grep_alias_path()? {
66 candidates.push(alias);
67 }
68 Ok(candidates)
69}
70
71pub fn resolve_ast_grep_binary_from_env_and_fs() -> Option<PathBuf> {
72 match AST_GREP_OVERRIDE.lock().unwrap_or_else(|e| e.into_inner()).clone() {
73 AstGrepBinaryOverride::System => {}
74 AstGrepBinaryOverride::Missing => return None,
75 AstGrepBinaryOverride::Path(path) => return Some(path),
76 }
77
78 let env_override = std::env::var_os(AST_GREP_BIN_ENV)
79 .filter(|value| !value.is_empty())
80 .map(PathBuf::from);
81
82 resolve_ast_grep_binary_with_sources(
83 env_override,
84 managed_ast_grep_candidates().unwrap_or_default(),
85 resolve_ast_grep_binary_on_path(),
86 )
87}
88
89pub fn resolve_ast_grep_binary_on_path() -> Option<PathBuf> {
90 which::which(canonical_ast_grep_binary_name())
91 .ok()
92 .or_else(|| alias_ast_grep_binary_name().and_then(|alias| which::which(alias).ok()))
93}
94
95pub fn canonical_ast_grep_binary_name() -> &'static str {
96 if cfg!(target_os = "windows") {
97 "ast-grep.exe"
98 } else {
99 "ast-grep"
100 }
101}
102
103pub fn alias_ast_grep_binary_name() -> Option<&'static str> {
104 if cfg!(target_os = "linux") {
105 None
106 } else if cfg!(target_os = "windows") {
107 Some("sg.exe")
108 } else {
109 Some("sg")
110 }
111}
112
113#[cold]
114pub fn missing_ast_grep_message(suffix: &str) -> String {
115 let extra = if suffix.is_empty() {
116 String::new()
117 } else {
118 format!(" {suffix}")
119 };
120 format!("ast-grep is not available; run `{AST_GREP_INSTALL_COMMAND}` or install `ast-grep` manually.{extra}")
121}
122
123pub(crate) fn is_binary_override_missing() -> bool {
127 matches!(AST_GREP_OVERRIDE.lock().unwrap_or_else(|e| e.into_inner()).clone(), AstGrepBinaryOverride::Missing)
128}
129
130pub const AST_GREP_NO_INSTALL_ENV: &str = "VTCODE_AST_GREP_NO_INSTALL";
133
134fn resolve_ast_grep_binary_with_sources(
135 env_override: Option<PathBuf>,
136 managed_candidates: Vec<PathBuf>,
137 path_candidate: Option<PathBuf>,
138) -> Option<PathBuf> {
139 env_override
140 .filter(|path| path.exists())
141 .or_else(|| managed_candidates.into_iter().find(|candidate| candidate.exists()))
142 .or(path_candidate)
143}
144
145#[cfg(test)]
146mod tests {
147 use super::{
148 alias_ast_grep_binary_name, canonical_ast_grep_binary_name, resolve_ast_grep_binary_from_env_and_fs,
149 resolve_ast_grep_binary_with_sources, set_ast_grep_binary_override_for_tests,
150 };
151 use std::path::PathBuf;
152 use tempfile::TempDir;
153
154 #[test]
155 fn canonical_binary_name_matches_platform() {
156 if cfg!(target_os = "windows") {
157 assert_eq!(canonical_ast_grep_binary_name(), "ast-grep.exe");
158 } else {
159 assert_eq!(canonical_ast_grep_binary_name(), "ast-grep");
160 }
161 }
162
163 #[test]
164 fn alias_binary_name_skips_linux() {
165 if cfg!(target_os = "linux") {
166 assert_eq!(alias_ast_grep_binary_name(), None);
167 } else if cfg!(target_os = "windows") {
168 assert_eq!(alias_ast_grep_binary_name(), Some("sg.exe"));
169 } else {
170 assert_eq!(alias_ast_grep_binary_name(), Some("sg"));
171 }
172 }
173
174 #[test]
175 fn resolution_prefers_env_override() {
176 let temp_dir = TempDir::new().expect("temp dir");
177 let env_path = temp_dir.path().join("custom-ast-grep");
178 std::fs::write(&env_path, "binary").expect("env binary");
179
180 let managed_path = temp_dir.path().join("managed-ast-grep");
181 std::fs::write(&managed_path, "binary").expect("managed binary");
182
183 let path_fallback = temp_dir.path().join("path-ast-grep");
184 std::fs::write(&path_fallback, "binary").expect("path binary");
185
186 let resolved =
187 resolve_ast_grep_binary_with_sources(Some(env_path.clone()), vec![managed_path], Some(path_fallback));
188
189 assert_eq!(resolved, Some(env_path));
190 }
191
192 #[test]
193 fn resolution_prefers_managed_binary_before_path() {
194 let temp_dir = TempDir::new().expect("temp dir");
195 let managed_path = temp_dir.path().join("managed-ast-grep");
196 std::fs::write(&managed_path, "binary").expect("managed binary");
197
198 let path_fallback = temp_dir.path().join("path-ast-grep");
199 std::fs::write(&path_fallback, "binary").expect("path binary");
200
201 let resolved = resolve_ast_grep_binary_with_sources(None, vec![managed_path.clone()], Some(path_fallback));
202
203 assert_eq!(resolved, Some(managed_path));
204 }
205
206 #[test]
207 fn resolution_uses_path_fallback_when_needed() {
208 let temp_dir = TempDir::new().expect("temp dir");
209 let path_fallback = temp_dir.path().join("path-ast-grep");
210 std::fs::write(&path_fallback, "binary").expect("path binary");
211
212 let resolved = resolve_ast_grep_binary_with_sources(
213 Some(PathBuf::from("/missing/env-ast-grep")),
214 vec![PathBuf::from("/missing/managed-ast-grep")],
215 Some(path_fallback.clone()),
216 );
217
218 assert_eq!(resolved, Some(path_fallback));
219 }
220
221 #[test]
222 fn resolution_uses_test_override_when_present() {
223 let temp_dir = TempDir::new().expect("temp dir");
224 let override_path = temp_dir.path().join("override-ast-grep");
225 std::fs::write(&override_path, "binary").expect("override binary");
226 let _guard = set_ast_grep_binary_override_for_tests(Some(override_path.clone()));
227
228 let resolved = resolve_ast_grep_binary_from_env_and_fs();
229
230 assert_eq!(resolved, Some(override_path));
231 }
232
233 #[test]
234 fn resolution_can_be_forced_missing_in_tests() {
235 let _guard = set_ast_grep_binary_override_for_tests(None);
236
237 let resolved = resolve_ast_grep_binary_from_env_and_fs();
238
239 assert_eq!(resolved, None);
240 }
241}