Skip to main content

tidepool_gvm/
go.rs

1// Go version management module
2use crate::{
3    downloader::Downloader,
4    symlink::{create_symlink, is_symlink, read_link, remove_symlink},
5    InstallRequest, ListInstalledRequest, RuntimeStatus, StatusRequest, SwitchRequest,
6    UninstallRequest, VersionList,
7};
8use anyhow::{anyhow, Result};
9use log::info;
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13/// Detailed information about a Go version
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
15pub struct GoVersionInfo {
16    /// Version number (e.g., "1.21.0")
17    pub version: String,
18    /// Operating system (e.g., "linux", "windows", "darwin")
19    pub os: String,
20    /// Architecture (e.g., "amd64", "arm64", "386")
21    pub arch: String,
22    /// File extension (e.g., "tar.gz", "zip")
23    pub extension: String,
24    /// Complete filename (e.g., "go1.21.0.linux-amd64.tar.gz")
25    pub filename: String,
26    /// Download URL
27    pub download_url: String,
28    /// Official SHA256 checksum
29    pub sha256: Option<String>,
30    /// File size in bytes
31    pub size: Option<u64>,
32    /// Whether it's installed
33    pub is_installed: bool,
34    /// Whether it's cached
35    pub is_cached: bool,
36    /// Whether it's the current active version
37    pub is_current: bool,
38    /// Local installation path (if installed)
39    pub install_path: Option<PathBuf>,
40    /// Cache file path (if cached)
41    pub cache_path: Option<PathBuf>,
42}
43
44pub struct GoManager {}
45
46impl Default for GoManager {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl GoManager {
53    #[must_use]
54    pub fn new() -> Self {
55        Self {}
56    }
57
58    /// Extract archive to specified directory
59    #[cfg(target_os = "windows")]
60    pub fn extract_archive(&self, archive_path: &Path, extract_to: &Path) -> Result<()> {
61        let file = std::fs::File::open(archive_path)?;
62        let mut archive = zip::ZipArchive::new(file)?;
63
64        for i in 0..archive.len() {
65            let mut file = archive.by_index(i)?;
66
67            let outpath = extract_to.join(file.name());
68
69            if file.name().ends_with('/') {
70                std::fs::create_dir_all(&outpath)?;
71            } else {
72                if let Some(p) = outpath.parent() {
73                    if !p.exists() {
74                        std::fs::create_dir_all(p)?;
75                    }
76                }
77                let mut outfile = std::fs::File::create(&outpath)?;
78                std::io::copy(&mut file, &mut outfile)?;
79            }
80        }
81
82        Ok(())
83    }
84
85    /// Extract archive to specified directory
86    #[cfg(not(target_os = "windows"))]
87    pub fn extract_archive(&self, archive_path: &Path, extract_to: &Path) -> Result<()> {
88        let file = std::fs::File::open(archive_path)?;
89
90        let gz = flate2::read::GzDecoder::new(file);
91        let mut tar = tar::Archive::new(gz);
92
93        tar.unpack(extract_to)?;
94
95        Ok(())
96    }
97
98    /// Switch to a specific Go version
99    pub fn switch_version(&self, version: &str, base_dir: &Path) -> Result<()> {
100        let version_path = base_dir.join(version);
101        let current_path = base_dir.join("current");
102
103        if !version_path.exists() {
104            return Err(anyhow!("Go version {} is not installed", version));
105        }
106
107        // Remove existing symlink if it exists
108        if current_path.exists() {
109            remove_symlink(&current_path)?;
110        }
111
112        // Create new symlink
113        create_symlink(&version_path, &current_path)?;
114
115        info!("Switched to Go version {version}");
116        Ok(())
117    }
118
119    /// Get current version
120    pub fn get_current_version(&self, base_dir: &Path) -> Option<String> {
121        let current_path = base_dir.join("current");
122        if current_path.exists() && is_symlink(&current_path) {
123            if let Ok(target) = read_link(&current_path) {
124                if let Some(name) = target.file_name() {
125                    return name.to_str().map(|s| s.to_string());
126                }
127            }
128        }
129        None
130    }
131
132    /// Get symlink target
133    pub fn get_link_target(&self, base_dir: &Path) -> Option<PathBuf> {
134        let current_path = base_dir.join("current");
135        if current_path.exists() && is_symlink(&current_path) {
136            read_link(&current_path).ok()
137        } else {
138            None
139        }
140    }
141
142    /// Get symlink information
143    pub fn get_symlink_info(&self, base_dir: &Path) -> String {
144        let current_path = base_dir.join("current");
145        if current_path.exists() && is_symlink(&current_path) {
146            if let Ok(target) = read_link(&current_path) {
147                return format!("{} -> {}", current_path.display(), target.display());
148            }
149        }
150        "No symlink found".to_string()
151    }
152
153    /// Install Go version
154    pub async fn install(&self, request: InstallRequest) -> Result<GoVersionInfo> {
155        let version = &request.version;
156        let install_dir = &request.install_dir;
157        let download_dir = &request.download_dir;
158
159        // Determine platform information
160        let platform = crate::platform::PlatformInfo::detect();
161        let filename = platform.archive_filename(version);
162        let download_url = format!("https://go.dev/dl/{filename}");
163        let archive_path = download_dir.join(&filename);
164
165        // Download if not cached
166        if !archive_path.exists() {
167            info!("Downloading Go {version} from {download_url}");
168
169            let downloader = Downloader::new();
170
171            downloader
172                .download_with_simple_progress(&download_url, &archive_path, &filename)
173                .await
174                .map_err(|e| anyhow::anyhow!("Download failed: {}", e))?;
175        }
176
177        // Extract archive
178        let version_dir = install_dir.join(version);
179        if version_dir.exists() && !request.force {
180            return Err(anyhow::anyhow!("Go version {} is already installed", version));
181        }
182
183        if version_dir.exists() {
184            std::fs::remove_dir_all(&version_dir)
185                .map_err(|e| anyhow::anyhow!("Failed to remove existing installation: {}", e))?;
186        }
187
188        // Create a temporary directory for extraction
189        let temp_extract_dir = install_dir.join(format!("{version}_temp"));
190
191        if temp_extract_dir.exists() {
192            std::fs::remove_dir_all(&temp_extract_dir)
193                .map_err(|e| anyhow::anyhow!("Failed to remove temp directory: {}", e))?;
194        }
195
196        std::fs::create_dir_all(&temp_extract_dir)
197            .map_err(|e| anyhow::anyhow!("Failed to create temp directory: {}", e))?;
198
199        // Extract to the temporary directory
200        info!("Extracting archive to {}", temp_extract_dir.display());
201        self.extract_archive(&archive_path, &temp_extract_dir)?;
202
203        // The official Go archive extracts into a "go" directory, which we need to rename to the version number
204        let extracted_go_dir = temp_extract_dir.join("go");
205        if !extracted_go_dir.exists() {
206            // Clean up the temporary directory
207            let _ = std::fs::remove_dir_all(&temp_extract_dir);
208            return Err(anyhow::anyhow!("Expected 'go' directory not found after extraction"));
209        }
210
211        // Rename the 'go' directory to the version directory
212        std::fs::rename(&extracted_go_dir, &version_dir).map_err(|e| {
213            anyhow::anyhow!("Failed to rename go directory to version directory: {}", e)
214        })?;
215
216        // Clean up the temporary directory
217        std::fs::remove_dir_all(&temp_extract_dir)
218            .map_err(|e| anyhow::anyhow!("Failed to remove temp directory: {}", e))?;
219
220        // Verify installation - the Go binary should now be in the bin subdirectory of the version directory
221        let go_binary =
222            version_dir.join("bin").join(crate::platform::PlatformInfo::go_executable_name());
223
224        if !go_binary.exists() {
225            return Err(anyhow::anyhow!(
226                "Go binary not found after extraction at {}",
227                go_binary.display()
228            ));
229        }
230
231        info!("Successfully installed Go version {version}");
232
233        // Automatically create or switch symlink to point to the newly installed version
234        let base_dir = install_dir;
235        let current_path = base_dir.join("current");
236        
237        // Check if current symlink already exists
238        let symlink_exists = current_path.exists() && is_symlink(&current_path);
239        
240        if symlink_exists {
241            // If symlink exists, update it to point to the new version
242            remove_symlink(&current_path)
243                .map_err(|e| anyhow::anyhow!("Failed to remove existing symlink: {}", e))?;
244            create_symlink(&version_dir, &current_path)
245                .map_err(|e| anyhow::anyhow!("Failed to create symlink: {}", e))?;
246            info!("Updated symlink to point to Go version {version}");
247        } else {
248            // If symlink doesn't exist, create it
249            create_symlink(&version_dir, &current_path)
250                .map_err(|e| anyhow::anyhow!("Failed to create symlink: {}", e))?;
251            info!("Created symlink pointing to Go version {version}");
252        }
253
254        Ok(GoVersionInfo {
255            version: version.to_string(),
256            os: platform.os,
257            arch: platform.arch,
258            extension: platform.extension,
259            filename: filename.clone(),
260            download_url,
261            sha256: None, // 可以在后续版本中添加校验和验证
262            size: None,   // 可以在下载时获取文件大小
263            is_installed: true,
264            is_cached: archive_path.exists(),
265            is_current: true, // 安装后自动激活
266            install_path: Some(version_dir),
267            cache_path: if archive_path.exists() { Some(archive_path) } else { None },
268        })
269    }
270
271    /// Switch to a version
272    pub fn switch_to(&self, request: SwitchRequest) -> Result<()> {
273        self.switch_version(&request.version, &request.base_dir)
274    }
275
276    /// Uninstall a version
277    pub fn uninstall(&self, request: UninstallRequest) -> Result<()> {
278        let version = &request.version;
279        let base_dir = &request.base_dir;
280        let version_path = base_dir.join(version);
281
282        if !version_path.exists() {
283            return Err(anyhow::anyhow!("Go version {} is not installed", version));
284        }
285
286        // Check if this is the current version
287        let current_path = base_dir.join("current");
288        if current_path.exists() && is_symlink(&current_path) {
289            if let Ok(target) = read_link(&current_path) {
290                if target == version_path {
291                    return Err(anyhow::anyhow!(
292                        "Cannot uninstall Go {} as it is currently active. Please switch to another version first.",
293                        version
294                    ));
295                }
296            }
297        }
298
299        // Remove the version directory
300        std::fs::remove_dir_all(&version_path)
301            .map_err(|e| anyhow::anyhow!("Failed to remove version directory: {}", e))?;
302
303        info!("Successfully uninstalled Go version {version}");
304        Ok(())
305    }
306
307    /// List installed versions
308    pub fn list_installed(&self, request: ListInstalledRequest) -> Result<VersionList> {
309        let base_dir = &request.base_dir;
310        let mut versions = Vec::new();
311
312        if !base_dir.exists() {
313            return Ok(VersionList { versions, total_count: 0 });
314        }
315
316        let current_version = self.get_current_version(base_dir);
317
318        for entry in std::fs::read_dir(base_dir)
319            .map_err(|e| anyhow::anyhow!("Failed to read directory: {}", e))?
320        {
321            let entry =
322                entry.map_err(|e| anyhow::anyhow!("Failed to read directory entry: {}", e))?;
323            let path = entry.path();
324
325            if path.is_dir() && path.file_name().is_some() {
326                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
327                    if name != "current" {
328                        let is_current = current_version.as_ref().is_some_and(|cv| cv == name);
329                        versions.push(GoVersionInfo {
330                            version: name.to_string(),
331                            os: std::env::consts::OS.to_string(),
332                            arch: std::env::consts::ARCH.to_string(),
333                            extension: String::new(),
334                            filename: String::new(),
335                            download_url: String::new(),
336                            sha256: None,
337                            size: None,
338                            is_installed: true,
339                            is_cached: false,
340                            is_current,
341                            install_path: Some(path.clone()),
342                            cache_path: None,
343                        });
344                    }
345                }
346            }
347        }
348
349        versions.sort();
350        let total_count = versions.len();
351
352        Ok(VersionList { versions, total_count })
353    }
354
355    /// List available versions
356    pub fn list_available(&self) -> Result<VersionList> {
357        // This is a simplified implementation
358        // In a real implementation, you would fetch the list from Go's official API
359        
360        // 获取当前版本以便标记
361        let base_dir = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from(".")).join(".gvm").join("versions");
362        let current_version = self.get_current_version(&base_dir);
363        
364        let mut versions = vec![
365            GoVersionInfo {
366                version: "1.21.3".to_string(),
367                os: "linux".to_string(),
368                arch: "amd64".to_string(),
369                extension: "tar.gz".to_string(),
370                filename: "go1.21.3.linux-amd64.tar.gz".to_string(),
371                download_url: String::new(),
372                sha256: None,
373                size: None,
374                is_installed: false,
375                is_cached: false,
376                is_current: false,
377                install_path: None,
378                cache_path: None,
379            },
380            GoVersionInfo {
381                version: "1.21.2".to_string(),
382                os: "linux".to_string(),
383                arch: "amd64".to_string(),
384                extension: "tar.gz".to_string(),
385                filename: "go1.21.2.linux-amd64.tar.gz".to_string(),
386                download_url: String::new(),
387                sha256: None,
388                size: None,
389                is_installed: false,
390                is_cached: false,
391                is_current: false,
392                install_path: None,
393                cache_path: None,
394            },
395            GoVersionInfo {
396                version: "1.21.1".to_string(),
397                os: "linux".to_string(),
398                arch: "amd64".to_string(),
399                extension: "tar.gz".to_string(),
400                filename: "go1.21.1.linux-amd64.tar.gz".to_string(),
401                download_url: String::new(),
402                sha256: None,
403                size: None,
404                is_installed: false,
405                is_cached: false,
406                is_current: false,
407                install_path: None,
408                cache_path: None,
409            },
410        ];
411        
412        // 标记当前版本
413        if let Some(ref current) = current_version {
414            for version in &mut versions {
415                version.is_current = version.version == *current;
416            }
417        }
418
419        let total_count = versions.len();
420        Ok(VersionList { versions, total_count })
421    }
422
423    /// Get status
424    pub fn status(&self, request: StatusRequest) -> Result<RuntimeStatus> {
425        let base_dir = request.base_dir.unwrap_or_else(|| {
426            dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")).join(".gvm").join("versions")
427        });
428
429        let current_version = self.get_current_version(&base_dir);
430        let mut environment_vars = HashMap::new();
431
432        if let Some(version) = &current_version {
433            let version_path = base_dir.join(version);
434            if version_path.exists() {
435                environment_vars.insert("GOROOT".to_string(), version_path.display().to_string());
436                environment_vars.insert(
437                    "PATH".to_string(),
438                    format!(
439                        "{};{}",
440                        version_path.join("bin").display(),
441                        std::env::var("PATH").unwrap_or_default()
442                    ),
443                );
444            }
445        }
446
447        let _link_info = if base_dir.join("current").exists() {
448            Some(self.get_symlink_info(&base_dir))
449        } else {
450            None
451        };
452
453        let _is_installed = current_version.is_some();
454        Ok(RuntimeStatus {
455            current_version,
456            current_path: self.get_link_target(&base_dir).map(|p| p.display().to_string()),
457            environment_vars,
458        })
459    }
460
461    /// Get version info
462    pub fn get_version_info(
463        &self,
464        version: &str,
465        install_dir: &Path,
466        cache_dir: &Path,
467    ) -> Result<GoVersionInfo> {
468        let platform = crate::platform::PlatformInfo::detect();
469        let filename = platform.archive_filename(version);
470        let download_url = format!("https://go.dev/dl/{filename}");
471
472        let install_path = install_dir.join(version);
473        let cache_path = cache_dir.join(&filename);
474
475        Ok(GoVersionInfo {
476            version: version.to_string(),
477            os: platform.os,
478            arch: platform.arch,
479            extension: platform.extension,
480            filename: filename.clone(),
481            download_url,
482            sha256: None,
483            size: None,
484            is_installed: install_path.exists(),
485            is_cached: cache_path.exists(),
486            is_current: false, // 这个方法不检查当前版本状态
487            install_path: if install_path.exists() { Some(install_path) } else { None },
488            cache_path: if cache_path.exists() { Some(cache_path) } else { None },
489        })
490    }
491}