1use std::path::PathBuf;
7use std::process::{Command, ExitStatus, Stdio};
8
9use crate::installer::{default_cache_dir, Installer};
10use crate::lockfile::LockedPackage;
11use crate::pep::Requirement;
12use crate::resolver::Resolver;
13use crate::venv::VenvManager;
14use crate::{Error, Result};
15
16use super::cache::{CachedTool, ToolCache};
17
18pub struct ToolRunner {
20 cache: ToolCache,
21 python: Option<PathBuf>,
23}
24
25impl ToolRunner {
26 pub fn new() -> Result<Self> {
28 Ok(Self {
29 cache: ToolCache::new()?,
30 python: None,
31 })
32 }
33
34 pub fn with_python(python: PathBuf) -> Result<Self> {
36 Ok(Self {
37 cache: ToolCache::new()?,
38 python: Some(python),
39 })
40 }
41
42 pub fn with_cache(cache: ToolCache) -> Self {
44 Self {
45 cache,
46 python: None,
47 }
48 }
49
50 pub fn cache(&self) -> &ToolCache {
52 &self.cache
53 }
54
55 pub async fn run(&self, package: &str, args: &[String]) -> Result<ExitStatus> {
60 self.run_with_command(package, package, args).await
61 }
62
63 pub async fn run_with_command(
68 &self,
69 package: &str,
70 command: &str,
71 args: &[String],
72 ) -> Result<ExitStatus> {
73 let tool = if let Some(cached) = self.cache.get(package) {
75 if cached.has_executable(command) {
76 tracing::debug!("Using cached tool: {}", package);
77 cached
78 } else {
79 self.install_tool(package).await?
81 }
82 } else {
83 self.install_tool(package).await?
85 };
86
87 self.execute(&tool, command, args)
89 }
90
91 async fn install_tool(&self, package: &str) -> Result<CachedTool> {
93 tracing::info!("Installing {}...", package);
94
95 let venv_path = self.cache.prepare(package)?;
97
98 let venv = VenvManager::new(&venv_path);
100 venv.create(self.python.as_deref())?;
101
102 let requirement = Requirement::parse(package).map_err(|e| {
104 Error::ToolExecutionFailed(format!("invalid package name {}: {}", package, e))
105 })?;
106
107 let resolver = Resolver::new();
109 let resolution = resolver.resolve(&[requirement]).await?;
110
111 let mut packages = std::collections::HashMap::new();
113 for pkg in &resolution.packages {
114 packages.insert(
115 pkg.name.clone(),
116 LockedPackage {
117 version: pkg.version.clone(),
118 url: if pkg.url.is_empty() {
119 None
120 } else {
121 Some(pkg.url.clone())
122 },
123 hash: if pkg.hash.is_empty() {
124 None
125 } else {
126 Some(pkg.hash.clone())
127 },
128 dependencies: pkg.dependencies.clone(),
129 markers: pkg.markers.clone(),
130 files: vec![],
131 },
132 );
133 }
134
135 let site_packages = venv.site_packages()?;
137 let installer = Installer::new(default_cache_dir());
138 let results = installer.install(&packages, &site_packages).await?;
139 tracing::debug!("Installed {} packages for {}", results.len(), package);
140
141 if let Some(pkg) = resolution.packages.iter().find(|p| {
143 p.name.to_lowercase() == package.to_lowercase()
144 || p.name.to_lowercase().replace('-', "_")
145 == package.to_lowercase().replace('-', "_")
146 }) {
147 self.cache.record_version(package, &pkg.version)?;
148 }
149
150 self.cache.get(package).ok_or_else(|| {
152 Error::ToolExecutionFailed(format!("failed to install tool: {}", package))
153 })
154 }
155
156 fn execute(&self, tool: &CachedTool, command: &str, args: &[String]) -> Result<ExitStatus> {
158 let executable = tool.executable(command);
159
160 if !executable.exists() {
161 let bin_dir = tool.bin_dir();
163 let available: Vec<_> = std::fs::read_dir(&bin_dir)
164 .map(|entries| {
165 entries
166 .filter_map(|e| e.ok())
167 .filter_map(|e| e.file_name().into_string().ok())
168 .filter(|n| {
169 !n.starts_with("python")
170 && !n.starts_with("pip")
171 && !n.starts_with("activate")
172 })
173 .collect()
174 })
175 .unwrap_or_default();
176
177 return Err(Error::ToolNotFound {
178 tool: format!(
179 "{} (available: {})",
180 command,
181 if available.is_empty() {
182 "none".to_string()
183 } else {
184 available.join(", ")
185 }
186 ),
187 });
188 }
189
190 let bin_dir = tool.bin_dir();
192 let current_path = std::env::var("PATH").unwrap_or_default();
193 let new_path = format!(
194 "{}{}{}",
195 bin_dir.display(),
196 if cfg!(windows) { ";" } else { ":" },
197 current_path
198 );
199
200 let status = Command::new(&executable)
201 .args(args)
202 .env("PATH", &new_path)
203 .env("VIRTUAL_ENV", &tool.venv_path)
204 .env_remove("PYTHONHOME")
205 .stdin(Stdio::inherit())
206 .stdout(Stdio::inherit())
207 .stderr(Stdio::inherit())
208 .status()
209 .map_err(|e| Error::ToolExecutionFailed(format!("failed to run {}: {}", command, e)))?;
210
211 Ok(status)
212 }
213
214 pub fn is_cached(&self, package: &str) -> bool {
216 self.cache.get(package).is_some()
217 }
218
219 pub fn list_cached(&self) -> Result<Vec<CachedTool>> {
221 self.cache.list()
222 }
223
224 pub fn clear(&self, package: &str) -> Result<bool> {
226 self.cache.clear(package)
227 }
228
229 pub fn clear_all(&self) -> Result<usize> {
231 self.cache.clear_all()
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use tempfile::tempdir;
239
240 #[test]
241 fn test_tool_runner_new() {
242 let runner = ToolRunner::new();
244 assert!(runner.is_ok());
245 }
246
247 #[test]
248 fn test_is_cached_false() {
249 let temp_dir = tempdir().unwrap();
250 let cache = ToolCache::with_dir(temp_dir.path().to_path_buf());
251 let runner = ToolRunner::with_cache(cache);
252
253 assert!(!runner.is_cached("nonexistent"));
254 }
255}