use crate::{
downloader::Downloader,
symlink::{create_symlink, is_symlink, read_link, remove_symlink},
InstallRequest, ListInstalledRequest, RuntimeStatus, StatusRequest, SwitchRequest,
UninstallRequest, VersionList,
};
use anyhow::{anyhow, Result};
use log::info;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
pub struct GoVersionInfo {
pub version: String,
pub os: String,
pub arch: String,
pub extension: String,
pub filename: String,
pub download_url: String,
pub sha256: Option<String>,
pub size: Option<u64>,
pub is_installed: bool,
pub is_cached: bool,
pub is_current: bool,
pub install_path: Option<PathBuf>,
pub cache_path: Option<PathBuf>,
}
pub struct GoManager {}
impl Default for GoManager {
fn default() -> Self {
Self::new()
}
}
impl GoManager {
#[must_use]
pub fn new() -> Self {
Self {}
}
#[cfg(target_os = "windows")]
pub fn extract_archive(&self, archive_path: &Path, extract_to: &Path) -> Result<()> {
let file = std::fs::File::open(archive_path)?;
let mut archive = zip::ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = extract_to.join(file.name());
if file.name().ends_with('/') {
std::fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
std::fs::create_dir_all(p)?;
}
}
let mut outfile = std::fs::File::create(&outpath)?;
std::io::copy(&mut file, &mut outfile)?;
}
}
Ok(())
}
#[cfg(not(target_os = "windows"))]
pub fn extract_archive(&self, archive_path: &Path, extract_to: &Path) -> Result<()> {
let file = std::fs::File::open(archive_path)?;
let gz = flate2::read::GzDecoder::new(file);
let mut tar = tar::Archive::new(gz);
tar.unpack(extract_to)?;
Ok(())
}
pub fn switch_version(&self, version: &str, base_dir: &Path) -> Result<()> {
let version_path = base_dir.join(version);
let current_path = base_dir.join("current");
if !version_path.exists() {
return Err(anyhow!("Go version {} is not installed", version));
}
if current_path.exists() {
remove_symlink(¤t_path)?;
}
create_symlink(&version_path, ¤t_path)?;
info!("Switched to Go version {version}");
Ok(())
}
pub fn get_current_version(&self, base_dir: &Path) -> Option<String> {
let current_path = base_dir.join("current");
if current_path.exists() && is_symlink(¤t_path) {
if let Ok(target) = read_link(¤t_path) {
if let Some(name) = target.file_name() {
return name.to_str().map(|s| s.to_string());
}
}
}
None
}
pub fn get_link_target(&self, base_dir: &Path) -> Option<PathBuf> {
let current_path = base_dir.join("current");
if current_path.exists() && is_symlink(¤t_path) {
read_link(¤t_path).ok()
} else {
None
}
}
pub fn get_symlink_info(&self, base_dir: &Path) -> String {
let current_path = base_dir.join("current");
if current_path.exists() && is_symlink(¤t_path) {
if let Ok(target) = read_link(¤t_path) {
return format!("{} -> {}", current_path.display(), target.display());
}
}
"No symlink found".to_string()
}
pub async fn install(&self, request: InstallRequest) -> Result<GoVersionInfo> {
let version = &request.version;
let install_dir = &request.install_dir;
let download_dir = &request.download_dir;
let platform = crate::platform::PlatformInfo::detect();
let filename = platform.archive_filename(version);
let download_url = format!("https://go.dev/dl/{filename}");
let archive_path = download_dir.join(&filename);
if !archive_path.exists() {
info!("Downloading Go {version} from {download_url}");
let downloader = Downloader::new();
downloader
.download_with_simple_progress(&download_url, &archive_path, &filename)
.await
.map_err(|e| anyhow::anyhow!("Download failed: {}", e))?;
}
let version_dir = install_dir.join(version);
if version_dir.exists() && !request.force {
return Err(anyhow::anyhow!("Go version {} is already installed", version));
}
if version_dir.exists() {
std::fs::remove_dir_all(&version_dir)
.map_err(|e| anyhow::anyhow!("Failed to remove existing installation: {}", e))?;
}
let temp_extract_dir = install_dir.join(format!("{version}_temp"));
if temp_extract_dir.exists() {
std::fs::remove_dir_all(&temp_extract_dir)
.map_err(|e| anyhow::anyhow!("Failed to remove temp directory: {}", e))?;
}
std::fs::create_dir_all(&temp_extract_dir)
.map_err(|e| anyhow::anyhow!("Failed to create temp directory: {}", e))?;
info!("Extracting archive to {}", temp_extract_dir.display());
self.extract_archive(&archive_path, &temp_extract_dir)?;
let extracted_go_dir = temp_extract_dir.join("go");
if !extracted_go_dir.exists() {
let _ = std::fs::remove_dir_all(&temp_extract_dir);
return Err(anyhow::anyhow!("Expected 'go' directory not found after extraction"));
}
std::fs::rename(&extracted_go_dir, &version_dir).map_err(|e| {
anyhow::anyhow!("Failed to rename go directory to version directory: {}", e)
})?;
std::fs::remove_dir_all(&temp_extract_dir)
.map_err(|e| anyhow::anyhow!("Failed to remove temp directory: {}", e))?;
let go_binary =
version_dir.join("bin").join(crate::platform::PlatformInfo::go_executable_name());
if !go_binary.exists() {
return Err(anyhow::anyhow!(
"Go binary not found after extraction at {}",
go_binary.display()
));
}
info!("Successfully installed Go version {version}");
let base_dir = install_dir;
let current_path = base_dir.join("current");
let symlink_exists = current_path.exists() && is_symlink(¤t_path);
if symlink_exists {
remove_symlink(¤t_path)
.map_err(|e| anyhow::anyhow!("Failed to remove existing symlink: {}", e))?;
create_symlink(&version_dir, ¤t_path)
.map_err(|e| anyhow::anyhow!("Failed to create symlink: {}", e))?;
info!("Updated symlink to point to Go version {version}");
} else {
create_symlink(&version_dir, ¤t_path)
.map_err(|e| anyhow::anyhow!("Failed to create symlink: {}", e))?;
info!("Created symlink pointing to Go version {version}");
}
Ok(GoVersionInfo {
version: version.to_string(),
os: platform.os,
arch: platform.arch,
extension: platform.extension,
filename: filename.clone(),
download_url,
sha256: None, size: None, is_installed: true,
is_cached: archive_path.exists(),
is_current: true, install_path: Some(version_dir),
cache_path: if archive_path.exists() { Some(archive_path) } else { None },
})
}
pub fn switch_to(&self, request: SwitchRequest) -> Result<()> {
self.switch_version(&request.version, &request.base_dir)
}
pub fn uninstall(&self, request: UninstallRequest) -> Result<()> {
let version = &request.version;
let base_dir = &request.base_dir;
let version_path = base_dir.join(version);
if !version_path.exists() {
return Err(anyhow::anyhow!("Go version {} is not installed", version));
}
let current_path = base_dir.join("current");
if current_path.exists() && is_symlink(¤t_path) {
if let Ok(target) = read_link(¤t_path) {
if target == version_path {
return Err(anyhow::anyhow!(
"Cannot uninstall Go {} as it is currently active. Please switch to another version first.",
version
));
}
}
}
std::fs::remove_dir_all(&version_path)
.map_err(|e| anyhow::anyhow!("Failed to remove version directory: {}", e))?;
info!("Successfully uninstalled Go version {version}");
Ok(())
}
pub fn list_installed(&self, request: ListInstalledRequest) -> Result<VersionList> {
let base_dir = &request.base_dir;
let mut versions = Vec::new();
if !base_dir.exists() {
return Ok(VersionList { versions, total_count: 0 });
}
let current_version = self.get_current_version(base_dir);
for entry in std::fs::read_dir(base_dir)
.map_err(|e| anyhow::anyhow!("Failed to read directory: {}", e))?
{
let entry =
entry.map_err(|e| anyhow::anyhow!("Failed to read directory entry: {}", e))?;
let path = entry.path();
if path.is_dir() && path.file_name().is_some() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name != "current" {
let is_current = current_version.as_ref().is_some_and(|cv| cv == name);
versions.push(GoVersionInfo {
version: name.to_string(),
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
extension: String::new(),
filename: String::new(),
download_url: String::new(),
sha256: None,
size: None,
is_installed: true,
is_cached: false,
is_current,
install_path: Some(path.clone()),
cache_path: None,
});
}
}
}
}
versions.sort();
let total_count = versions.len();
Ok(VersionList { versions, total_count })
}
pub fn list_available(&self) -> Result<VersionList> {
let base_dir = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from(".")).join(".gvm").join("versions");
let current_version = self.get_current_version(&base_dir);
let mut versions = vec![
GoVersionInfo {
version: "1.21.3".to_string(),
os: "linux".to_string(),
arch: "amd64".to_string(),
extension: "tar.gz".to_string(),
filename: "go1.21.3.linux-amd64.tar.gz".to_string(),
download_url: String::new(),
sha256: None,
size: None,
is_installed: false,
is_cached: false,
is_current: false,
install_path: None,
cache_path: None,
},
GoVersionInfo {
version: "1.21.2".to_string(),
os: "linux".to_string(),
arch: "amd64".to_string(),
extension: "tar.gz".to_string(),
filename: "go1.21.2.linux-amd64.tar.gz".to_string(),
download_url: String::new(),
sha256: None,
size: None,
is_installed: false,
is_cached: false,
is_current: false,
install_path: None,
cache_path: None,
},
GoVersionInfo {
version: "1.21.1".to_string(),
os: "linux".to_string(),
arch: "amd64".to_string(),
extension: "tar.gz".to_string(),
filename: "go1.21.1.linux-amd64.tar.gz".to_string(),
download_url: String::new(),
sha256: None,
size: None,
is_installed: false,
is_cached: false,
is_current: false,
install_path: None,
cache_path: None,
},
];
if let Some(ref current) = current_version {
for version in &mut versions {
version.is_current = version.version == *current;
}
}
let total_count = versions.len();
Ok(VersionList { versions, total_count })
}
pub fn status(&self, request: StatusRequest) -> Result<RuntimeStatus> {
let base_dir = request.base_dir.unwrap_or_else(|| {
dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")).join(".gvm").join("versions")
});
let current_version = self.get_current_version(&base_dir);
let mut environment_vars = HashMap::new();
if let Some(version) = ¤t_version {
let version_path = base_dir.join(version);
if version_path.exists() {
environment_vars.insert("GOROOT".to_string(), version_path.display().to_string());
environment_vars.insert(
"PATH".to_string(),
format!(
"{};{}",
version_path.join("bin").display(),
std::env::var("PATH").unwrap_or_default()
),
);
}
}
let _link_info = if base_dir.join("current").exists() {
Some(self.get_symlink_info(&base_dir))
} else {
None
};
let _is_installed = current_version.is_some();
Ok(RuntimeStatus {
current_version,
current_path: self.get_link_target(&base_dir).map(|p| p.display().to_string()),
environment_vars,
})
}
pub fn get_version_info(
&self,
version: &str,
install_dir: &Path,
cache_dir: &Path,
) -> Result<GoVersionInfo> {
let platform = crate::platform::PlatformInfo::detect();
let filename = platform.archive_filename(version);
let download_url = format!("https://go.dev/dl/{filename}");
let install_path = install_dir.join(version);
let cache_path = cache_dir.join(&filename);
Ok(GoVersionInfo {
version: version.to_string(),
os: platform.os,
arch: platform.arch,
extension: platform.extension,
filename: filename.clone(),
download_url,
sha256: None,
size: None,
is_installed: install_path.exists(),
is_cached: cache_path.exists(),
is_current: false, install_path: if install_path.exists() { Some(install_path) } else { None },
cache_path: if cache_path.exists() { Some(cache_path) } else { None },
})
}
}