vx_tool_uv/
uv_tool.rs

1//! UV tool implementations - Python package management tools
2
3use std::collections::HashMap;
4use vx_core::{
5    GitHubVersionParser, HttpUtils, Result, ToolContext, ToolExecutionResult, UvUrlBuilder,
6    VersionInfo, VxEnvironment, VxError, VxTool,
7};
8
9/// Macro to generate UV tool implementations using VxTool trait
10macro_rules! uv_vx_tool {
11    ($name:ident, $cmd:literal, $desc:literal, $homepage:expr) => {
12        #[derive(Debug, Clone)]
13        pub struct $name {
14            _url_builder: UvUrlBuilder,
15            _version_parser: GitHubVersionParser,
16        }
17
18        impl $name {
19            pub fn new() -> Self {
20                Self {
21                    _url_builder: UvUrlBuilder::new(),
22                    _version_parser: GitHubVersionParser::new("astral-sh", "uv"),
23                }
24            }
25        }
26
27        #[async_trait::async_trait]
28        impl VxTool for $name {
29            fn name(&self) -> &str {
30                $cmd
31            }
32
33            fn description(&self) -> &str {
34                $desc
35            }
36
37            fn aliases(&self) -> Vec<&str> {
38                vec![]
39            }
40
41            async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
42                // For UV, fetch from GitHub releases
43                let json = HttpUtils::fetch_json(UvUrlBuilder::versions_url()).await?;
44                GitHubVersionParser::parse_versions(&json, include_prerelease)
45            }
46
47            async fn install_version(&self, version: &str, force: bool) -> Result<()> {
48                if !force && self.is_version_installed(version).await? {
49                    return Err(vx_core::VxError::VersionAlreadyInstalled {
50                        tool_name: self.name().to_string(),
51                        version: version.to_string(),
52                    });
53                }
54
55                let install_dir = self.get_version_install_dir(version);
56                let _exe_path = self.default_install_workflow(version, &install_dir).await?;
57
58                // Verify installation
59                if !self.is_version_installed(version).await? {
60                    return Err(vx_core::VxError::InstallationFailed {
61                        tool_name: self.name().to_string(),
62                        version: version.to_string(),
63                        message: "Installation verification failed".to_string(),
64                    });
65                }
66
67                Ok(())
68            }
69
70            async fn is_version_installed(&self, version: &str) -> Result<bool> {
71                let env = VxEnvironment::new().expect("Failed to create VX environment");
72
73                // For uvx, check if uv is installed (they are the same binary)
74                if self.name() == "uvx" {
75                    return Ok(env.is_version_installed("uv", version));
76                }
77
78                Ok(env.is_version_installed(self.name(), version))
79            }
80
81            async fn get_active_version(&self) -> Result<String> {
82                let env = VxEnvironment::new().expect("Failed to create VX environment");
83
84                // For uvx, use uv version
85                if self.name() == "uvx" {
86                    if let Some(active_version) = env.get_active_version("uv")? {
87                        return Ok(active_version);
88                    }
89
90                    let installed_versions = env.list_installed_versions("uv")?;
91                    return installed_versions.first().cloned().ok_or_else(|| {
92                        VxError::ToolNotInstalled {
93                            tool_name: "uv".to_string(),
94                        }
95                    });
96                }
97
98                // For uv, use default implementation
99                if let Some(active_version) = env.get_active_version(self.name())? {
100                    return Ok(active_version);
101                }
102
103                let installed_versions = env.list_installed_versions(self.name())?;
104                installed_versions
105                    .first()
106                    .cloned()
107                    .ok_or_else(|| VxError::ToolNotInstalled {
108                        tool_name: self.name().to_string(),
109                    })
110            }
111
112            async fn get_installed_versions(&self) -> Result<Vec<String>> {
113                let env = VxEnvironment::new().expect("Failed to create VX environment");
114
115                // For uvx, use uv versions
116                if self.name() == "uvx" {
117                    return env.list_installed_versions("uv");
118                }
119
120                env.list_installed_versions(self.name())
121            }
122
123            async fn execute(
124                &self,
125                args: &[String],
126                context: &ToolContext,
127            ) -> Result<ToolExecutionResult> {
128                // For uvx, find executable in uv installation
129                if self.name() == "uvx" && !context.use_system_path {
130                    let active_version = self.get_active_version().await?;
131                    let env = VxEnvironment::new().expect("Failed to create VX environment");
132                    let uv_install_dir = env.get_version_install_dir("uv", &active_version);
133
134                    // uvx is actually the uv binary with tool run behavior
135                    let exe_path = env.find_executable_in_dir(&uv_install_dir, "uv")?;
136
137                    // Execute with tool run as the first arguments
138                    let mut cmd = std::process::Command::new(&exe_path);
139                    cmd.arg("tool");
140                    cmd.arg("run");
141                    cmd.args(args);
142
143                    if let Some(cwd) = &context.working_directory {
144                        cmd.current_dir(cwd);
145                    }
146
147                    for (key, value) in &context.environment_variables {
148                        cmd.env(key, value);
149                    }
150
151                    let status = cmd.status().map_err(|e| VxError::Other {
152                        message: format!("Failed to execute {}: {}", self.name(), e),
153                    })?;
154
155                    return Ok(ToolExecutionResult {
156                        exit_code: status.code().unwrap_or(1),
157                        stdout: None,
158                        stderr: None,
159                    });
160                }
161
162                // For uv or system path execution, use default workflow
163                self.default_execute_workflow(args, context).await
164            }
165
166            async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
167                if version == "latest" {
168                    // For latest, get the actual latest version first
169                    let versions = self.fetch_versions(false).await?;
170                    if let Some(latest_version) = versions.first() {
171                        return Ok(UvUrlBuilder::download_url(&latest_version.version));
172                    }
173                    return Ok(None);
174                }
175                Ok(UvUrlBuilder::download_url(version))
176            }
177
178            fn metadata(&self) -> HashMap<String, String> {
179                let mut meta = HashMap::new();
180                meta.insert("homepage".to_string(), $homepage.unwrap_or("").to_string());
181                meta.insert("ecosystem".to_string(), "python".to_string());
182                meta.insert(
183                    "repository".to_string(),
184                    "https://github.com/astral-sh/uv".to_string(),
185                );
186                meta.insert("license".to_string(), "MIT OR Apache-2.0".to_string());
187                meta
188            }
189        }
190
191        impl Default for $name {
192            fn default() -> Self {
193                Self::new()
194            }
195        }
196    };
197}
198
199// Define UV tools using the VxTool macro
200uv_vx_tool!(
201    UvCommand,
202    "uv",
203    "An extremely fast Python package installer and resolver",
204    Some("https://docs.astral.sh/uv/")
205);
206uv_vx_tool!(
207    UvxTool,
208    "uvx",
209    "Python application runner",
210    Some("https://docs.astral.sh/uv/")
211);
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_uv_tool_creation() {
219        let tool = UvCommand::new();
220        assert_eq!(tool.name(), "uv");
221        assert!(!tool.description().is_empty());
222    }
223
224    #[test]
225    fn test_uvx_tool_creation() {
226        let tool = UvxTool::new();
227        assert_eq!(tool.name(), "uvx");
228        assert!(!tool.description().is_empty());
229    }
230
231    #[test]
232    fn test_uv_tool_metadata() {
233        let tool = UvCommand::new();
234        let metadata = tool.metadata();
235
236        assert!(metadata.contains_key("homepage"));
237        assert!(metadata.contains_key("ecosystem"));
238        assert_eq!(metadata.get("ecosystem"), Some(&"python".to_string()));
239    }
240}