1use 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
15pub struct GoVersionInfo {
16 pub version: String,
18 pub os: String,
20 pub arch: String,
22 pub extension: String,
24 pub filename: String,
26 pub download_url: String,
28 pub sha256: Option<String>,
30 pub size: Option<u64>,
32 pub is_installed: bool,
34 pub is_cached: bool,
36 pub is_current: bool,
38 pub install_path: Option<PathBuf>,
40 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 #[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 #[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 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 if current_path.exists() {
109 remove_symlink(¤t_path)?;
110 }
111
112 create_symlink(&version_path, ¤t_path)?;
114
115 info!("Switched to Go version {version}");
116 Ok(())
117 }
118
119 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(¤t_path) {
123 if let Ok(target) = read_link(¤t_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 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(¤t_path) {
136 read_link(¤t_path).ok()
137 } else {
138 None
139 }
140 }
141
142 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(¤t_path) {
146 if let Ok(target) = read_link(¤t_path) {
147 return format!("{} -> {}", current_path.display(), target.display());
148 }
149 }
150 "No symlink found".to_string()
151 }
152
153 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 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 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 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 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 info!("Extracting archive to {}", temp_extract_dir.display());
201 self.extract_archive(&archive_path, &temp_extract_dir)?;
202
203 let extracted_go_dir = temp_extract_dir.join("go");
205 if !extracted_go_dir.exists() {
206 let _ = std::fs::remove_dir_all(&temp_extract_dir);
208 return Err(anyhow::anyhow!("Expected 'go' directory not found after extraction"));
209 }
210
211 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 std::fs::remove_dir_all(&temp_extract_dir)
218 .map_err(|e| anyhow::anyhow!("Failed to remove temp directory: {}", e))?;
219
220 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 let base_dir = install_dir;
235 let current_path = base_dir.join("current");
236
237 let symlink_exists = current_path.exists() && is_symlink(¤t_path);
239
240 if symlink_exists {
241 remove_symlink(¤t_path)
243 .map_err(|e| anyhow::anyhow!("Failed to remove existing symlink: {}", e))?;
244 create_symlink(&version_dir, ¤t_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 create_symlink(&version_dir, ¤t_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, size: None, is_installed: true,
264 is_cached: archive_path.exists(),
265 is_current: true, install_path: Some(version_dir),
267 cache_path: if archive_path.exists() { Some(archive_path) } else { None },
268 })
269 }
270
271 pub fn switch_to(&self, request: SwitchRequest) -> Result<()> {
273 self.switch_version(&request.version, &request.base_dir)
274 }
275
276 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 let current_path = base_dir.join("current");
288 if current_path.exists() && is_symlink(¤t_path) {
289 if let Ok(target) = read_link(¤t_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 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 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 pub fn list_available(&self) -> Result<VersionList> {
357 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 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 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) = ¤t_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 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, 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}