use anyhow::{bail, Context, Result};
use std::path::PathBuf;
use crate::{
config::Config,
fs as gvsn_fs, lock,
user_version::VersionSpec,
version::{parse_tool_versions_golang_line, GoVersion, GO_VERSION_FILE, TOOL_VERSIONS_FILE},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VersionSource {
Local,
Global,
}
impl VersionSource {
pub fn label(self) -> &'static str {
match self {
Self::Local => "local (.go-version/.tool-versions)",
Self::Global => "global",
}
}
}
pub fn list_installed(config: &Config) -> Result<Vec<GoVersion>> {
let dir = config.versions_dir();
if !dir.exists() {
return Ok(vec![]);
}
let mut versions: Vec<GoVersion> = std::fs::read_dir(&dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.filter_map(|e| {
e.file_name()
.into_string()
.ok()
.and_then(|n| GoVersion::parse(&n).ok())
})
.collect();
versions.sort_by(|a, b| b.cmp(a));
Ok(versions)
}
pub fn is_installed(config: &Config, version: &GoVersion) -> bool {
config.version_dir(&version.tag()).exists()
}
pub fn global_version(config: &Config) -> Result<GoVersion> {
let path = config.version_file();
if !path.exists() {
bail!("No global Go version set. Run 'gvsn use <version>'.");
}
let raw = std::fs::read_to_string(&path)?.trim().to_string();
GoVersion::parse(&raw).context("Corrupted version file - run 'gvsn use <version>'")
}
pub fn active_version(config: &Config) -> Result<(GoVersion, VersionSource)> {
let mut dir = std::env::current_dir()?;
let mut depth = 0u8;
loop {
let go_version_path = dir.join(GO_VERSION_FILE);
if go_version_path.exists() {
let raw = std::fs::read_to_string(&go_version_path)
.with_context(|| format!("Cannot read {}", go_version_path.display()))?;
let v = GoVersion::parse(raw.trim()).context("Corrupted .go-version")?;
return Ok((v, VersionSource::Local));
}
let tool_versions_path = dir.join(TOOL_VERSIONS_FILE);
if tool_versions_path.exists() {
let content = std::fs::read_to_string(&tool_versions_path)
.with_context(|| format!("Cannot read {}", tool_versions_path.display()))?;
if let Some(raw) = parse_tool_versions_golang_line(&content) {
let v =
GoVersion::parse(raw).context("Corrupted golang entry in .tool-versions")?;
return Ok((v, VersionSource::Local));
}
}
if depth >= 20 || !dir.pop() {
break;
}
depth += 1;
}
global_version(config).map(|v| (v, VersionSource::Global))
}
pub fn version_bin_path(config: &Config, version: &GoVersion) -> Result<PathBuf> {
let bin = config.version_bin_dir(&version.tag());
if !bin.exists() {
bail!(
"Go {} is not installed. Run 'gvsn install {}'.",
version,
version
);
}
Ok(bin)
}
fn write_global_version_file(config: &Config, version: &GoVersion) -> Result<()> {
std::fs::write(config.version_file(), version.tag())
.context("Failed to write global version file")
}
fn write_current_link(config: &Config, version: &GoVersion) -> Result<()> {
let link = config.current_dir();
let target = config.version_dir(&version.tag());
gvsn_fs::set_version_link(&link, &target)
.with_context(|| format!("Failed to update current link to {}", version.tag()))
}
pub fn set_active_version(config: &Config, version: &GoVersion) -> Result<()> {
let lock_path = config.root.join(".lock");
lock::with_lock(&lock_path, || {
write_global_version_file(config, version)?;
write_current_link(config, version)
})
}
pub fn resolve_installed(config: &Config, spec: &VersionSpec) -> Result<GoVersion> {
let installed = list_installed(config)?;
match spec {
VersionSpec::Latest => installed
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("No Go versions installed.")),
_ => installed
.into_iter()
.find(|v| spec.matches(v))
.ok_or_else(|| {
anyhow::anyhow!("Go {} is not installed. Run 'gvsn install {}'.", spec, spec)
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn make_config() -> (tempfile::TempDir, Config) {
let dir = tempdir().unwrap();
let config = Config {
root: dir.path().to_path_buf(),
};
(dir, config)
}
fn install(config: &Config, tag: &str) {
std::fs::create_dir_all(config.version_dir(tag)).unwrap();
}
#[test]
fn list_installed_returns_empty_when_dir_missing() {
let (_dir, config) = make_config();
assert_eq!(list_installed(&config).unwrap(), vec![]);
}
#[test]
fn list_installed_sorts_newest_first_and_skips_garbage() {
let (_dir, config) = make_config();
install(&config, "go1.21.0");
install(&config, "go1.22.4");
install(&config, "go1.22.3");
std::fs::create_dir_all(config.versions_dir().join("not-a-version")).unwrap();
let versions = list_installed(&config).unwrap();
let tags: Vec<String> = versions.iter().map(GoVersion::tag).collect();
assert_eq!(tags, vec!["go1.22.4", "go1.22.3", "go1.21"]);
}
#[test]
fn is_installed_true_and_false() {
let (_dir, config) = make_config();
install(&config, "go1.22.4");
let v = GoVersion::parse("1.22.4").unwrap();
assert!(is_installed(&config, &v));
let missing = GoVersion::parse("1.19.0").unwrap();
assert!(!is_installed(&config, &missing));
}
#[test]
fn global_version_errors_when_unset() {
let (_dir, config) = make_config();
assert!(global_version(&config).is_err());
}
#[test]
fn global_version_reads_written_tag() {
let (_dir, config) = make_config();
std::fs::write(config.version_file(), "go1.22.4").unwrap();
let v = global_version(&config).unwrap();
assert_eq!(v.tag(), "go1.22.4");
}
#[test]
fn global_version_errors_on_corrupted_file() {
let (_dir, config) = make_config();
std::fs::write(config.version_file(), "not-a-version").unwrap();
assert!(global_version(&config).is_err());
}
#[test]
fn version_bin_path_errors_when_not_installed() {
let (_dir, config) = make_config();
let v = GoVersion::parse("1.22.4").unwrap();
assert!(version_bin_path(&config, &v).is_err());
}
#[test]
fn version_bin_path_succeeds_when_installed() {
let (_dir, config) = make_config();
let tag = "go1.22.4";
std::fs::create_dir_all(config.version_bin_dir(tag)).unwrap();
let v = GoVersion::parse("1.22.4").unwrap();
let bin = version_bin_path(&config, &v).unwrap();
assert_eq!(bin, config.version_bin_dir(tag));
}
#[test]
fn set_active_version_writes_tag_to_file() {
let (_dir, config) = make_config();
let tag = "go1.22.4";
install(&config, tag);
let v = GoVersion::parse("1.22.4").unwrap();
set_active_version(&config, &v).unwrap();
assert_eq!(
std::fs::read_to_string(config.version_file()).unwrap(),
"go1.22.4"
);
}
#[test]
fn set_active_version_points_current_link_to_version_dir() {
let (_dir, config) = make_config();
let tag = "go1.22.4";
install(&config, tag);
std::fs::write(config.version_dir(tag).join("marker.txt"), b"x").unwrap();
let v = GoVersion::parse("1.22.4").unwrap();
set_active_version(&config, &v).unwrap();
assert!(config.current_dir().join("marker.txt").exists());
}
#[test]
fn resolve_installed_latest_picks_newest() {
let (_dir, config) = make_config();
install(&config, "go1.21.0");
install(&config, "go1.22.4");
let resolved = resolve_installed(&config, &VersionSpec::Latest).unwrap();
assert_eq!(resolved.tag(), "go1.22.4");
}
#[test]
fn resolve_installed_latest_errors_when_none_installed() {
let (_dir, config) = make_config();
assert!(resolve_installed(&config, &VersionSpec::Latest).is_err());
}
#[test]
fn resolve_installed_partial_matches_installed_patch() {
let (_dir, config) = make_config();
install(&config, "go1.22.4");
let spec = VersionSpec::Partial {
major: 1,
minor: 22,
};
let resolved = resolve_installed(&config, &spec).unwrap();
assert_eq!(resolved.tag(), "go1.22.4");
}
#[test]
fn resolve_installed_exact_errors_when_not_found() {
let (_dir, config) = make_config();
install(&config, "go1.22.4");
let spec = VersionSpec::Exact {
major: 1,
minor: 19,
patch: 0,
};
assert!(resolve_installed(&config, &spec).is_err());
}
#[test]
fn version_source_label_text() {
assert_eq!(
VersionSource::Local.label(),
"local (.go-version/.tool-versions)"
);
assert_eq!(VersionSource::Global.label(), "global");
}
}