1use crate::{
3 downloader::{Downloader, ProgressReporter},
4 InstallRequest, ListInstalledRequest, RuntimeStatus, StatusRequest, SwitchRequest,
5 UninstallRequest, VersionInfo, VersionList, VersionManager,
6};
7use log::{debug, info, warn};
8use sha2::{Digest, Sha256};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use tokio::io::AsyncReadExt;
12
13#[derive(Debug, Clone)]
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 install_path: Option<PathBuf>,
38 pub cache_path: Option<PathBuf>,
40}
41
42pub struct GoManager {}
43
44impl Default for GoManager {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl GoManager {
51 #[must_use]
52 pub fn new() -> Self {
53 Self {}
54 }
55 pub fn extract_archive(&self, archive_path: &Path, extract_to: &Path) -> Result<(), String> {
65 #[cfg(target_os = "windows")]
66 {
67 Self::extract_zip(archive_path, extract_to)
68 }
69
70 #[cfg(not(target_os = "windows"))]
71 {
72 self.extract_tar_gz(archive_path, extract_to)
73 }
74 }
75 pub fn switch_version(&self, version: &str, base_dir: &Path) -> Result<(), String> {
85 #[cfg(target_os = "windows")]
86 {
87 Self::switch_version_windows(version, base_dir)
88 }
89
90 #[cfg(not(target_os = "windows"))]
91 {
92 self.switch_version_unix(version, base_dir)
93 }
94 }
95 #[cfg(target_os = "windows")]
97 fn switch_version_windows(version: &str, base_dir: &Path) -> Result<(), String> {
98 let version_path = base_dir.join(version);
99 let junction_path = base_dir.join("current");
100
101 if !version_path.exists() {
102 return Err(format!("Go version {version} is not installed"));
103 }
104
105 let go_exe = version_path.join("bin").join("go.exe");
107 if !go_exe.exists() {
108 return Err(format!(
109 "Invalid Go installation: missing go.exe in {}",
110 version_path.display()
111 ));
112 }
113
114 debug!("Creating junction point for Go version {version}");
115
116 if junction_path.exists() && junction_path.is_dir() {
118 std::fs::remove_dir_all(&junction_path)
119 .map_err(|e| format!("Failed to remove existing directory: {e}"))?;
120 }
121
122 let output = std::process::Command::new("cmd")
124 .args([
125 "/C",
126 "mklink",
127 "/J",
128 &junction_path.to_string_lossy(),
129 &version_path.to_string_lossy(),
130 ])
131 .output()
132 .map_err(|e| format!("Failed to execute mklink: {e}"))?;
133
134 if !output.status.success() {
135 let error_msg = String::from_utf8_lossy(&output.stderr);
136 return Err(format!("Failed to create junction: {error_msg}"));
137 }
138
139 info!("Successfully created junction point for Go version {version}");
140
141 if !junction_path.exists() {
143 return Err("Junction does not exist after creation".to_string());
144 }
145
146 let junction_go_exe = junction_path.join("bin").join("go.exe");
147 if !junction_go_exe.exists() {
148 return Err("Junction target is invalid: missing go.exe".to_string());
149 }
150 debug!("Successfully created junction point for Go version {version}");
151 debug!("Environment variables updated for Go version {version}");
152 Ok(())
153 }
154
155 #[cfg(not(target_os = "windows"))]
157 fn switch_version_unix(&self, version: &str, base_dir: &Path) -> Result<(), String> {
158 let version_path = base_dir.join(version);
159 let current_path = base_dir.join("current");
160
161 if !version_path.exists() {
162 return Err(format!("Go version {} is not installed", version));
163 }
164
165 let go_binary = version_path.join("bin").join("go");
167 if !go_binary.exists() {
168 return Err(format!(
169 "Invalid Go installation: missing go binary in {}",
170 version_path.display()
171 ));
172 }
173
174 debug!("Creating symlink for Go version {}", version);
175
176 if current_path.exists() {
178 if current_path.is_symlink() {
179 std::fs::remove_file(¤t_path)
180 .map_err(|e| format!("Failed to remove existing symlink: {}", e))?;
181 } else if current_path.is_dir() {
182 std::fs::remove_dir_all(¤t_path)
183 .map_err(|e| format!("Failed to remove existing directory: {}", e))?;
184 }
185 }
186
187 #[cfg(not(target_os = "windows"))]
189 {
190 std::os::unix::fs::symlink(&version_path, ¤t_path)
191 .map_err(|e| format!("Failed to create symlink: {}", e))?;
192 }
193
194 info!("Successfully created symlink for Go version {}", version);
195
196 if !current_path.exists() {
198 return Err("Symlink does not exist after creation".to_string());
199 }
200
201 let current_go_binary = current_path.join("bin").join("go");
202 if !current_go_binary.exists() {
203 return Err("Symlink target is invalid: missing go binary".to_string());
204 }
205
206 debug!("Successfully created symlink for Go version {}", version);
207 Ok(())
208 }
209 #[must_use]
211 pub fn get_current_version(&self, base_dir: &Path) -> Option<String> {
212 if let Some(target) = self.get_link_target(base_dir) {
214 return target
215 .file_name()
216 .and_then(|name| name.to_str())
217 .map(std::string::ToString::to_string);
218 }
219
220 if let Ok(goroot) = std::env::var("GOROOT") {
222 let goroot_path = PathBuf::from(goroot);
223 if goroot_path.starts_with(base_dir) {
224 return goroot_path
225 .file_name()
226 .and_then(|name| name.to_str())
227 .map(std::string::ToString::to_string);
228 }
229 }
230
231 None
232 }
233 #[must_use]
235 pub fn get_link_target(&self, base_dir: &Path) -> Option<PathBuf> {
236 let link_path = base_dir.join("current");
237
238 if !link_path.exists() {
239 return None;
240 }
241
242 if link_path.is_symlink() {
244 if let Ok(target) = std::fs::read_link(&link_path) {
246 return Some(target);
247 }
248 }
249
250 None
251 }
252 #[cfg(target_os = "windows")]
254 #[must_use]
255 pub fn get_junction_target(&self, base_dir: &Path) -> Option<PathBuf> {
256 self.get_link_target(base_dir)
257 }
258 #[must_use]
260 pub fn get_symlink_target(&self, base_dir: &Path) -> Option<PathBuf> {
261 self.get_link_target(base_dir)
262 }
263 #[must_use]
265 pub fn get_symlink_info(&self, base_dir: &Path) -> String {
266 let link_path = base_dir.join("current");
267
268 if !link_path.exists() {
269 #[cfg(target_os = "windows")]
270 return "No junction found".to_string();
271 #[cfg(not(target_os = "windows"))]
272 return "No symlink found".to_string();
273 }
274
275 if let Some(target) = self.get_link_target(base_dir) {
276 #[cfg(target_os = "windows")]
277 return format!("Junction: {} -> {}", link_path.display(), target.display());
278 #[cfg(not(target_os = "windows"))]
279 return format!("Symlink: {} -> {}", link_path.display(), target.display());
280 }
281
282 #[cfg(target_os = "windows")]
283 return "Junction exists but target unknown".to_string();
284 #[cfg(not(target_os = "windows"))]
285 return "Symlink exists but target unknown".to_string();
286 }
287
288 #[cfg(target_os = "windows")]
290 fn extract_zip(zip_path: &Path, extract_to: &Path) -> Result<(), String> {
291 use std::fs::File;
292 use std::io::BufReader;
293
294 let file = File::open(zip_path).map_err(|e| format!("Failed to open zip file: {e}"))?;
295 let reader = BufReader::new(file);
296
297 let mut archive =
298 zip::ZipArchive::new(reader).map_err(|e| format!("Failed to read zip archive: {e}"))?;
299
300 for i in 0..archive.len() {
301 let mut file = archive
302 .by_index(i)
303 .map_err(|e| format!("Failed to access file in archive: {e}"))?;
304 let Some(file_path) = file.enclosed_name() else { continue }; let Ok(relative_path) = file_path.strip_prefix("go") else {
306 continue; };
308
309 if relative_path.as_os_str().is_empty() {
311 continue;
312 }
313
314 let outpath = extract_to.join(relative_path);
315
316 if file.name().ends_with('/') {
317 std::fs::create_dir_all(&outpath)
319 .map_err(|e| format!("Failed to create directory: {e}"))?;
320 } else {
321 if let Some(p) = outpath.parent() {
323 if !p.exists() {
324 std::fs::create_dir_all(p)
325 .map_err(|e| format!("Failed to create parent directory: {e}"))?;
326 }
327 }
328
329 let mut outfile = File::create(&outpath)
330 .map_err(|e| format!("Failed to create output file: {e}"))?;
331
332 std::io::copy(&mut file, &mut outfile)
333 .map_err(|e| format!("Failed to extract file: {e}"))?;
334 }
335 }
336
337 Ok(())
338 }
339
340 #[cfg(not(target_os = "windows"))]
342 fn extract_tar_gz(&self, tar_gz_path: &Path, extract_to: &Path) -> Result<(), String> {
343 use flate2::read::GzDecoder;
344 use std::fs::File;
345 use tar::Archive;
346
347 let file =
348 File::open(tar_gz_path).map_err(|e| format!("Failed to open tar.gz file: {}", e))?;
349
350 let gz = GzDecoder::new(file);
351 let mut archive = Archive::new(gz);
352
353 for entry in
355 archive.entries().map_err(|e| format!("Failed to read archive entries: {}", e))?
356 {
357 let mut entry = entry.map_err(|e| format!("Failed to read archive entry: {}", e))?;
358
359 let entry_path =
360 entry.path().map_err(|e| format!("Failed to get entry path: {}", e))?;
361
362 let relative_path = if let Ok(stripped) = entry_path.strip_prefix("go") {
364 stripped
365 } else {
366 continue; };
368
369 if relative_path.as_os_str().is_empty() {
371 continue;
372 }
373
374 let target_path = extract_to.join(relative_path);
375
376 if let Some(parent) = target_path.parent() {
378 std::fs::create_dir_all(parent)
379 .map_err(|e| format!("Failed to create parent directory: {}", e))?;
380 }
381
382 entry.unpack(&target_path).map_err(|e| format!("Failed to extract entry: {}", e))?;
384 }
385
386 Ok(())
387 }
388 pub async fn calculate_file_hash(&self, file_path: &Path) -> Result<String, String> {
397 let mut file = tokio::fs::File::open(file_path)
398 .await
399 .map_err(|e| format!("Failed to open file for hash calculation: {e}"))?;
400
401 let mut hasher = Sha256::new();
402 let mut buffer = vec![0u8; 8192]; loop {
405 let bytes_read =
406 file.read(&mut buffer).await.map_err(|e| format!("Error reading file: {e}"))?;
407
408 if bytes_read == 0 {
409 break;
410 }
411
412 hasher.update(&buffer[..bytes_read]);
413 }
414
415 let result = hasher.finalize();
416 Ok(format!("{result:x}"))
417 }
418
419 async fn get_official_checksum(
421 &self,
422 version: &str,
423 os: &str,
424 arch: &str,
425 extension: &str,
426 ) -> Result<String, String> {
427 let client = reqwest::Client::new();
428 let checksums_url = "https://go.dev/dl/?mode=json&include=all".to_string();
429
430 debug!("Fetching official checksums: {checksums_url}");
431
432 let response = client
433 .get(&checksums_url)
434 .send()
435 .await
436 .map_err(|e| format!("Failed to fetch checksums: {e}"))?;
437
438 let versions: serde_json::Value =
439 response.json().await.map_err(|e| format!("Failed to parse checksum data: {e}"))?;
440
441 let filename = format!("go{version}.{os}-{arch}.{extension}");
442 debug!("Looking for checksum for file: {filename}");
443
444 if let Some(releases) = versions.as_array() {
446 for release in releases {
447 if let Some(version_str) = release.get("version").and_then(|v| v.as_str()) {
448 if version_str == format!("go{version}") {
449 if let Some(files) = release.get("files").and_then(|f| f.as_array()) {
450 for file in files {
451 if let Some(file_name) =
452 file.get("filename").and_then(|f| f.as_str())
453 {
454 if file_name == filename {
455 if let Some(sha256) =
456 file.get("sha256").and_then(|s| s.as_str())
457 {
458 debug!("Found official checksum: {sha256}");
459 return Ok(sha256.to_string());
460 }
461 }
462 }
463 }
464 }
465 }
466 }
467 }
468 }
469
470 Err(format!("Official checksum not found for Go {version} ({filename})"))
471 }
472
473 async fn verify_file_integrity(
475 &self,
476 file_path: &Path,
477 version: &str,
478 os: &str,
479 arch: &str,
480 extension: &str,
481 ) -> Result<(), String> {
482 debug!("Starting file integrity verification: {}", file_path.display());
483
484 let file_hash = self.calculate_file_hash(file_path).await?;
486 debug!("File hash: {file_hash}");
487
488 let official_hash = self.get_official_checksum(version, os, arch, extension).await?;
490 debug!("Official hash: {official_hash}");
491
492 if file_hash.to_lowercase() == official_hash.to_lowercase() {
494 info!("File integrity verification passed");
495 Ok(())
496 } else {
497 Err(format!(
498 "File integrity verification failed!\nExpected: {official_hash}\nActual: {file_hash}"
499 ))
500 }
501 }
502 #[must_use]
505 pub fn validate_cache_file(&self, file_path: &Path) -> bool {
506 if !file_path.exists() {
507 return false;
508 }
509
510 match std::fs::metadata(file_path) {
511 Ok(metadata) => {
512 if metadata.len() < 1024 {
514 return false;
515 }
516
517 std::fs::File::open(file_path).is_ok()
519 }
520 Err(_) => false,
521 }
522 }
523 pub async fn get_version_info(
533 &self,
534 version: &str,
535 install_dir: &Path,
536 cache_dir: &Path,
537 ) -> Result<GoVersionInfo, String> {
538 use crate::downloader::Downloader;
539
540 let (os, arch) = if cfg!(target_os = "windows") {
542 ("windows", if cfg!(target_arch = "x86_64") { "amd64" } else { "386" })
543 } else if cfg!(target_os = "macos") {
544 ("darwin", if cfg!(target_arch = "x86_64") { "amd64" } else { "arm64" })
545 } else {
546 ("linux", if cfg!(target_arch = "x86_64") { "amd64" } else { "386" })
547 };
548
549 let extension = if cfg!(target_os = "windows") { "zip" } else { "tar.gz" };
550 let filename = format!("go{version}.{os}-{arch}.{extension}");
551 let download_url = format!("https://go.dev/dl/{filename}");
552
553 let install_path = install_dir.join(version);
555 let is_installed = install_path.exists() && {
556 let go_binary = if cfg!(target_os = "windows") {
557 install_path.join("bin").join("go.exe")
558 } else {
559 install_path.join("bin").join("go")
560 };
561 go_binary.exists()
562 };
563
564 let cache_path = cache_dir.join(&filename);
566 let is_cached = cache_path.exists() && self.validate_cache_file(&cache_path);
567
568 let sha256 = self.get_official_checksum(version, os, arch, extension).await.ok();
570
571 let size = if is_cached {
573 debug!("Getting size from cached file: {}", cache_path.display());
575 std::fs::metadata(&cache_path).ok().map(|m| m.len())
576 } else {
577 debug!("Getting size from network for: {download_url}");
579 let downloader = Downloader::new();
580 match downloader.get_file_size(&download_url).await {
581 Ok(size) => {
582 debug!("Successfully got file size from network: {size} bytes");
583 Some(size)
584 }
585 Err(e) => {
586 debug!("Failed to get file size from network: {e}");
587 None
588 }
589 }
590 };
591
592 Ok(GoVersionInfo {
593 version: version.to_string(),
594 os: os.to_string(),
595 arch: arch.to_string(),
596 extension: extension.to_string(),
597 filename,
598 download_url,
599 sha256,
600 size,
601 is_installed,
602 is_cached,
603 install_path: if is_installed { Some(install_path) } else { None },
604 cache_path: if is_cached { Some(cache_path) } else { None },
605 })
606 }
607}
608
609#[async_trait::async_trait]
610impl VersionManager for GoManager {
611 #[allow(clippy::too_many_lines)]
613 async fn install(&self, request: InstallRequest) -> Result<VersionInfo, String> {
614 let version = &request.version;
615 let install_dir = &request.install_dir;
616 let download_dir = &request.download_dir;
617 let force = request.force;
618
619 if !install_dir.exists() {
621 return Err("Install directory does not exist".to_string());
622 }
623
624 if !install_dir.is_dir() {
625 return Err("Install path is not a directory".to_string());
626 }
627
628 let version_dir = install_dir.join(version);
630 if version_dir.exists() && !force {
631 return Err(format!("Go version {version} is already installed"));
632 }
633
634 if force && version_dir.exists() {
636 std::fs::remove_dir_all(&version_dir)
637 .map_err(|e| format!("Failed to remove existing version directory: {e}"))?;
638 }
639
640 std::fs::create_dir_all(&version_dir)
641 .map_err(|e| format!("Failed to create version directory: {e}"))?;
642
643 let (os, arch) = if cfg!(target_os = "windows") {
645 ("windows", if cfg!(target_arch = "x86_64") { "amd64" } else { "386" })
646 } else if cfg!(target_os = "macos") {
647 ("darwin", if cfg!(target_arch = "x86_64") { "amd64" } else { "arm64" })
648 } else {
649 ("linux", if cfg!(target_arch = "x86_64") { "amd64" } else { "386" })
650 };
651
652 let extension = if cfg!(target_os = "windows") { "zip" } else { "tar.gz" };
653 let download_url = format!("https://go.dev/dl/go{version}.{os}-{arch}.{extension}");
654
655 let archive_name = format!("go{version}.{os}-{arch}.{extension}");
657
658 if !download_dir.exists() {
660 return Err(format!(
661 "Download directory does not exist: {}. Please ensure the download directory is created before installation.",
662 download_dir.display()
663 ));
664 }
665
666 if !download_dir.is_dir() {
667 return Err(format!("Download path is not a directory: {}", download_dir.display()));
668 }
669
670 let download_path = download_dir.join(&archive_name);
671
672 let need_download = if force {
674 if download_path.exists() {
676 debug!("Force mode: removing existing cached file");
677 std::fs::remove_file(&download_path)
678 .map_err(|e| format!("Failed to remove cached file: {e}"))?;
679 }
680 debug!("Force mode: downloading Go {version} from {download_url}");
681 true
682 } else if download_path.exists() {
683 debug!("Found cached file: {}", download_path.display());
684
685 if self.validate_cache_file(&download_path) {
687 let metadata = std::fs::metadata(&download_path).unwrap();
688 debug!("Using valid cached file (size: {} bytes)", metadata.len());
689 false
690 } else {
691 debug!("Cached file appears to be corrupted or incomplete, will re-download");
692 true
693 }
694 } else {
695 debug!("Downloading Go {version} from {download_url}");
696 true
697 };
698
699 if need_download {
701 let downloader = Downloader::new();
703
704 let file_size =
706 downloader.get_file_size(&download_url).await.map_err(|e| format!("{e}"))?;
707 let progress_reporter = ProgressReporter::new(file_size);
708
709 downloader
710 .download(&download_url, &download_path, Some(progress_reporter))
711 .await
712 .map_err(|e| format!("{e}"))?;
713
714 debug!("Verifying downloaded file integrity");
716 self.verify_file_integrity(&download_path, version, os, arch, extension)
717 .await
718 .map_err(|e| {
719 if download_path.exists() {
721 let _ = std::fs::remove_file(&download_path);
722 warn!(
723 "Verification failed, deleted corrupted file: {}",
724 download_path.display()
725 );
726 }
727 format!("File integrity verification failed: {e}")
728 })?;
729 } else {
730 debug!("Verifying cached file integrity");
732 self.verify_file_integrity(&download_path, version, os, arch, extension)
733 .await
734 .map_err(|e| {
735 if download_path.exists() {
737 let _ = std::fs::remove_file(&download_path);
738 warn!(
739 "Cached file verification failed, deleted: {}",
740 download_path.display()
741 );
742 }
743 format!(
744 "Cached file integrity verification failed, recommend re-downloading: {e}"
745 )
746 })?;
747 }
748
749 debug!("Extracting Go {} to {}", version, version_dir.display());
750
751 self.extract_archive(&download_path, &version_dir)?;
753
754 info!("Go {} installed successfully", version);
758
759 Ok(VersionInfo { version: version.to_string(), install_path: version_dir })
761 }
762 fn switch_to(&self, request: SwitchRequest) -> Result<(), String> {
764 let version = &request.version;
765 let base_dir = &request.base_dir;
766 self.switch_version(version, base_dir)
767 }
768
769 fn uninstall(&self, request: UninstallRequest) -> Result<(), String> {
771 let version = &request.version;
772 let base_dir = &request.base_dir;
773 let version_path = base_dir.join(version);
774
775 if !version_path.exists() {
776 return Err(format!("Go version {version} is not installed"));
777 }
778
779 if let Some(current_version) = self.get_current_version(base_dir) {
781 if current_version == *version {
782 return Err(format!(
783 "Cannot uninstall Go {version} as it is currently active. Please switch to another version or clear the current symlink first."
784 ));
785 }
786 }
787
788 std::fs::remove_dir_all(&version_path)
789 .map_err(|e| format!("Failed to remove Go {version}: {e}"))?;
790
791 Ok(())
792 }
793
794 fn list_installed(&self, request: ListInstalledRequest) -> Result<VersionList, String> {
804 let base_dir = &request.base_dir;
805 if !base_dir.exists() {
806 return Err(format!("Base directory does not exist: {}", base_dir.display()));
807 }
808
809 let mut versions = Vec::new();
810
811 match std::fs::read_dir(base_dir) {
812 Ok(entries) => {
813 for entry in entries.flatten() {
814 let name = entry.file_name().to_string_lossy().to_string();
815
816 if name == "current" {
818 continue;
819 }
820
821 if entry.path().is_dir() {
823 let go_binary_name =
824 if cfg!(target_os = "windows") { "go.exe" } else { "go" };
825 let go_binary = entry.path().join("bin").join(go_binary_name);
826 if go_binary.exists() {
827 versions.push(name);
828 }
829 }
830 }
831 }
832 Err(e) => return Err(format!("Failed to read directory: {e}")),
833 }
834
835 versions.sort();
836 let total_count = versions.len();
837
838 Ok(VersionList { versions, total_count })
839 }
840
841 async fn list_available(&self) -> Result<VersionList, String> {
843 let url = "https://go.dev/dl/?mode=json";
845
846 let resp = reqwest::get(url).await.map_err(|e| format!("{e}"))?;
847 let releases: serde_json::Value = resp.json().await.map_err(|e| format!("{e}"))?;
848
849 let mut versions = Vec::new();
850
851 if let Some(array) = releases.as_array() {
852 for release in array {
853 if let Some(version_str) = release["version"].as_str() {
854 if let Some(version) = version_str.strip_prefix("go") {
856 versions.push(version.to_string());
857 }
858 }
859 }
860 }
861
862 versions.retain(|v| !v.contains("beta") && !v.contains("rc"));
864
865 versions.sort_by(|a, b| {
867 let a_parts: Vec<u32> = a.split('.').filter_map(|s| s.parse().ok()).collect();
869 let b_parts: Vec<u32> = b.split('.').filter_map(|s| s.parse().ok()).collect();
870 b_parts.cmp(&a_parts)
871 });
872
873 let total_count = versions.len();
874
875 Ok(VersionList { versions, total_count })
876 }
877
878 fn status(&self, request: StatusRequest) -> Result<RuntimeStatus, String> {
880 let base_dir = request.base_dir.as_deref();
881 let mut environment_vars = HashMap::new();
882
883 let goroot = std::env::var("GOROOT").unwrap_or_else(|_| "Not set".to_string());
884 let gopath = std::env::var("GOPATH").unwrap_or_else(|_| "Not set".to_string());
885
886 environment_vars.insert("GOROOT".to_string(), goroot.clone());
887 environment_vars.insert("GOPATH".to_string(), gopath);
888 let mut current_version = None;
889 let mut install_path = None;
890 #[cfg(target_os = "windows")]
891 let mut link_info = None;
892 #[cfg(not(target_os = "windows"))]
893 let link_info = None;
894
895 if let Some(base_dir) = base_dir {
896 current_version = self.get_current_version(base_dir);
897
898 if let Some(ref version) = current_version {
899 install_path = Some(base_dir.join(version));
900 }
901 #[cfg(target_os = "windows")]
902 {
903 link_info = Some(self.get_symlink_info(base_dir));
904 }
905 }
906
907 Ok(RuntimeStatus { current_version, install_path, environment_vars, link_info })
908 }
909}