vx_installer/
installer.rs

1//! Installation utilities and configuration
2
3use crate::{
4    downloader::Downloader,
5    formats::ArchiveExtractor,
6    progress::{ProgressContext, ProgressStyle},
7    Error, Result,
8};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13/// Main installer for tools and packages
14pub struct Installer {
15    downloader: Downloader,
16    extractor: ArchiveExtractor,
17}
18
19impl Installer {
20    /// Create a new installer
21    pub async fn new() -> Result<Self> {
22        let downloader = Downloader::new()?;
23        let extractor = ArchiveExtractor::new();
24
25        Ok(Self {
26            downloader,
27            extractor,
28        })
29    }
30
31    /// Install a tool using the provided configuration
32    pub async fn install(&self, config: &InstallConfig) -> Result<PathBuf> {
33        // Check if already installed and not forcing reinstall
34        if !config.force && self.is_installed(config).await? {
35            return Err(Error::AlreadyInstalled {
36                tool_name: config.tool_name.clone(),
37                version: config.version.clone(),
38            });
39        }
40
41        // Create progress context
42        let progress = ProgressContext::new(
43            crate::progress::create_progress_reporter(ProgressStyle::default(), true),
44            true,
45        );
46
47        match &config.install_method {
48            InstallMethod::Archive { format: _ } => {
49                self.install_from_archive(config, &progress).await
50            }
51            InstallMethod::Binary => self.install_binary(config, &progress).await,
52            InstallMethod::Script { url } => self.install_from_script(config, url, &progress).await,
53            InstallMethod::PackageManager { manager, package } => {
54                self.install_from_package_manager(config, manager, package, &progress)
55                    .await
56            }
57            InstallMethod::Custom { method } => {
58                self.install_custom(config, method, &progress).await
59            }
60        }
61    }
62
63    /// Check if a tool version is already installed
64    pub async fn is_installed(&self, config: &InstallConfig) -> Result<bool> {
65        let install_dir = &config.install_dir;
66
67        // Check if installation directory exists and contains executables
68        if !install_dir.exists() {
69            return Ok(false);
70        }
71
72        // Look for executable files
73        let bin_dir = install_dir.join("bin");
74        if bin_dir.exists() {
75            let exe_name = if cfg!(windows) {
76                format!("{}.exe", config.tool_name)
77            } else {
78                config.tool_name.clone()
79            };
80
81            let exe_path = bin_dir.join(&exe_name);
82            Ok(exe_path.exists() && exe_path.is_file())
83        } else {
84            // Check if there are any executable files in the install directory
85            self.has_executables(install_dir)
86        }
87    }
88
89    /// Uninstall a tool
90    pub async fn uninstall(&self, _tool_name: &str, install_dir: &Path) -> Result<()> {
91        if install_dir.exists() {
92            std::fs::remove_dir_all(install_dir)?;
93        }
94        Ok(())
95    }
96
97    /// Install from archive (ZIP, TAR, etc.)
98    async fn install_from_archive(
99        &self,
100        config: &InstallConfig,
101        progress: &ProgressContext,
102    ) -> Result<PathBuf> {
103        let download_url = config
104            .download_url
105            .as_ref()
106            .ok_or_else(|| Error::InvalidConfig {
107                message: "Download URL is required for archive installation".to_string(),
108            })?;
109
110        // Download the archive
111        let temp_path = self
112            .downloader
113            .download_temp(download_url, progress)
114            .await?;
115
116        // Extract the archive
117        let extracted_files = self
118            .extractor
119            .extract(&temp_path, &config.install_dir, progress)
120            .await?;
121
122        // Find the best executable
123        let executable_path = self
124            .extractor
125            .find_best_executable(&extracted_files, &config.tool_name)?;
126
127        // Clean up temporary file
128        let _ = std::fs::remove_file(temp_path);
129
130        Ok(executable_path)
131    }
132
133    /// Install binary file directly
134    async fn install_binary(
135        &self,
136        config: &InstallConfig,
137        progress: &ProgressContext,
138    ) -> Result<PathBuf> {
139        let download_url = config
140            .download_url
141            .as_ref()
142            .ok_or_else(|| Error::InvalidConfig {
143                message: "Download URL is required for binary installation".to_string(),
144            })?;
145
146        // Create bin directory
147        let bin_dir = config.install_dir.join("bin");
148        std::fs::create_dir_all(&bin_dir)?;
149
150        // Determine executable name
151        let exe_name = if cfg!(windows) {
152            format!("{}.exe", config.tool_name)
153        } else {
154            config.tool_name.clone()
155        };
156
157        let exe_path = bin_dir.join(&exe_name);
158
159        // Download directly to the target location
160        self.downloader
161            .download(download_url, &exe_path, progress)
162            .await?;
163
164        // Make executable on Unix systems
165        #[cfg(unix)]
166        {
167            use std::os::unix::fs::PermissionsExt;
168            let metadata = std::fs::metadata(&exe_path)?;
169            let mut permissions = metadata.permissions();
170            permissions.set_mode(0o755);
171            std::fs::set_permissions(&exe_path, permissions)?;
172        }
173
174        Ok(exe_path)
175    }
176
177    /// Install from script
178    async fn install_from_script(
179        &self,
180        _config: &InstallConfig,
181        _script_url: &str,
182        _progress: &ProgressContext,
183    ) -> Result<PathBuf> {
184        // TODO: Implement script-based installation
185        Err(Error::unsupported_format("script installation"))
186    }
187
188    /// Install using package manager
189    async fn install_from_package_manager(
190        &self,
191        _config: &InstallConfig,
192        _manager: &str,
193        _package: &str,
194        _progress: &ProgressContext,
195    ) -> Result<PathBuf> {
196        // TODO: Implement package manager installation
197        Err(Error::unsupported_format("package manager installation"))
198    }
199
200    /// Install using custom method
201    async fn install_custom(
202        &self,
203        _config: &InstallConfig,
204        _method: &str,
205        _progress: &ProgressContext,
206    ) -> Result<PathBuf> {
207        // TODO: Implement custom installation methods
208        Err(Error::unsupported_format("custom installation"))
209    }
210
211    /// Check if directory contains executable files
212    fn has_executables(&self, dir: &Path) -> Result<bool> {
213        if !dir.exists() {
214            return Ok(false);
215        }
216
217        for entry in walkdir::WalkDir::new(dir).max_depth(3) {
218            let entry = entry?;
219            let path = entry.path();
220
221            if path.is_file() {
222                #[cfg(unix)]
223                {
224                    use std::os::unix::fs::PermissionsExt;
225                    if let Ok(metadata) = std::fs::metadata(path) {
226                        let permissions = metadata.permissions();
227                        if permissions.mode() & 0o111 != 0 {
228                            return Ok(true);
229                        }
230                    }
231                }
232
233                #[cfg(windows)]
234                {
235                    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
236                        if matches!(ext.to_lowercase().as_str(), "exe" | "bat" | "cmd" | "com") {
237                            return Ok(true);
238                        }
239                    }
240                }
241            }
242        }
243
244        Ok(false)
245    }
246}
247
248/// Configuration for tool installation
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct InstallConfig {
251    /// Name of the tool to install
252    pub tool_name: String,
253
254    /// Version to install
255    pub version: String,
256
257    /// Installation method
258    pub install_method: InstallMethod,
259
260    /// Download URL (if applicable)
261    pub download_url: Option<String>,
262
263    /// Installation directory
264    pub install_dir: PathBuf,
265
266    /// Whether to force reinstallation
267    pub force: bool,
268
269    /// Checksum for verification
270    pub checksum: Option<String>,
271
272    /// Additional configuration
273    pub metadata: HashMap<String, String>,
274}
275
276/// Different methods for installing tools
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub enum InstallMethod {
279    /// Download and extract archive
280    Archive { format: ArchiveFormat },
281
282    /// Use system package manager
283    PackageManager { manager: String, package: String },
284
285    /// Run installation script
286    Script { url: String },
287
288    /// Download single binary
289    Binary,
290
291    /// Custom installation method
292    Custom { method: String },
293}
294
295/// Supported archive formats
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub enum ArchiveFormat {
298    Zip,
299    TarGz,
300    TarXz,
301    TarBz2,
302    SevenZip,
303}
304
305/// Builder for InstallConfig
306pub struct InstallConfigBuilder {
307    config: InstallConfig,
308}
309
310impl Default for InstallConfigBuilder {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316impl InstallConfigBuilder {
317    /// Create a new builder
318    pub fn new() -> Self {
319        Self {
320            config: InstallConfig {
321                tool_name: String::new(),
322                version: String::new(),
323                install_method: InstallMethod::Binary,
324                download_url: None,
325                install_dir: PathBuf::new(),
326                force: false,
327                checksum: None,
328                metadata: HashMap::new(),
329            },
330        }
331    }
332
333    /// Set the tool name
334    pub fn tool_name(mut self, name: impl Into<String>) -> Self {
335        self.config.tool_name = name.into();
336        self
337    }
338
339    /// Set the version
340    pub fn version(mut self, version: impl Into<String>) -> Self {
341        self.config.version = version.into();
342        self
343    }
344
345    /// Set the installation method
346    pub fn install_method(mut self, method: InstallMethod) -> Self {
347        self.config.install_method = method;
348        self
349    }
350
351    /// Set the download URL
352    pub fn download_url(mut self, url: impl Into<String>) -> Self {
353        self.config.download_url = Some(url.into());
354        self
355    }
356
357    /// Set the installation directory
358    pub fn install_dir(mut self, dir: impl Into<PathBuf>) -> Self {
359        self.config.install_dir = dir.into();
360        self
361    }
362
363    /// Set force reinstallation
364    pub fn force(mut self, force: bool) -> Self {
365        self.config.force = force;
366        self
367    }
368
369    /// Set checksum
370    pub fn checksum(mut self, checksum: impl Into<String>) -> Self {
371        self.config.checksum = Some(checksum.into());
372        self
373    }
374
375    /// Add metadata
376    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
377        self.config.metadata.insert(key.into(), value.into());
378        self
379    }
380
381    /// Build the configuration
382    pub fn build(self) -> InstallConfig {
383        self.config
384    }
385}
386
387impl InstallConfig {
388    /// Create a new builder
389    pub fn builder() -> InstallConfigBuilder {
390        InstallConfigBuilder::new()
391    }
392}