diff --git a/e2e/backend/test_go_install_slow b/e2e/backend/test_go_install_slow index 22464dccda..7c24d0b1de 100644 --- a/e2e/backend/test_go_install_slow +++ b/e2e/backend/test_go_install_slow @@ -15,7 +15,7 @@ assert_fail go cat >>.mise.toml <&1'" "Kratos: An elegant toolkit for Go microservices." + assert_contains "mise x go:github.com/golang-migrate/migrate/v4/cmd/migrate[tags=postgres]@4.18.2 -- bash -c 'migrate --help 2>&1'" "postgres" # Required to properly cleanup as go installs read-only sources diff --git a/src/backend/go.rs b/src/backend/go.rs index a9268a5881..4abd01e2c8 100644 --- a/src/backend/go.rs +++ b/src/backend/go.rs @@ -2,14 +2,17 @@ use crate::backend::Backend; use crate::backend::VersionInfo; use crate::backend::backend_type::BackendType; use crate::backend::platform_target::PlatformTarget; +use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::Config; use crate::config::Settings; +use crate::hash::hash_to_str; use crate::install_context::InstallContext; use crate::timeout; use crate::toolset::{ToolRequest, ToolVersion}; use async_trait::async_trait; +use dashmap::DashMap; use itertools::Itertools; use std::collections::BTreeMap; use std::{fmt::Debug, sync::Arc}; @@ -18,6 +21,7 @@ use xx::regex; #[derive(Debug)] pub struct GoBackend { ba: Arc, + module_versions_cache: DashMap>>>, } #[async_trait] @@ -52,6 +56,14 @@ impl Backend for GoBackend { timeout::run_with_timeout_async( async || { let tool_name = self.tool_name(); + + // First try the exact tool path. If this succeeds but returns no versions, + // treat that as authoritative so installs can continue with `@latest` + // instead of resolving to a parent module version. + if let Some(versions) = self.fetch_go_module_versions(config, &tool_name).await? { + return Ok(versions); + } + let parts = tool_name.split('/').collect::>(); let module_root_index = if parts[0] == "github.com" { // Try likely module root index first @@ -75,32 +87,13 @@ impl Backend for GoBackend { for i in indices { let mod_path = parts[..=i].join("/"); - let res = cmd!( - "go", - "list", - "-mod=readonly", - "-m", - "-versions", - "-json", - mod_path - ) - .full_env(self.dependency_env(config).await?) - .read(); - if let Ok(raw) = res { - let res = serde_json::from_str::(&raw); - if let Ok(mod_info) = res { - // remove the leading v from the versions - let versions = mod_info - .versions - .into_iter() - .map(|v| VersionInfo { - version: v.trim_start_matches('v').to_string(), - ..Default::default() - }) - .collect(); - return Ok(versions); - } - }; + if mod_path == tool_name { + continue; + } + if let Some(versions) = self.fetch_go_module_versions(config, &mod_path).await? + { + return Ok(versions); + } } Ok(vec![]) @@ -113,7 +106,7 @@ impl Backend for GoBackend { async fn install_version_( &self, ctx: &InstallContext, - tv: ToolVersion, + mut tv: ToolVersion, ) -> eyre::Result { // Check if go is available self.warn_if_dependency_missing( @@ -125,6 +118,21 @@ impl Backend for GoBackend { ) .await; + // Some deep modules return no Versions from `go list -versions`. + // If the original request was `latest`, force `@latest` install for + // those modules instead of using a parent module's resolved version. + let mut install_version = tv.version.clone(); + if tv.request.version() == "latest" + && tv.version != "latest" + && self + .fetch_go_module_versions(&ctx.config, &self.tool_name()) + .await? + .is_some_and(|v| v.is_empty()) + { + install_version = "latest".to_string(); + tv.version = "latest".to_string(); + } + let opts = self.ba.opts(); let install = async |v| { @@ -142,17 +150,17 @@ impl Backend for GoBackend { }; // try "v" prefix if the version starts with semver - let use_v = regex!(r"^\d+\.\d+\.\d+").is_match(&tv.version); + let use_v = regex!(r"^\d+\.\d+\.\d+").is_match(&install_version); if use_v { - if install(format!("v{}", tv.version)).await.is_err() { + if install(format!("v{}", install_version)).await.is_err() { warn!("Failed to install, trying again without added 'v' prefix"); } else { return Ok(tv); } } - install(tv.version.clone()).await?; + install(install_version).await?; Ok(tv) } @@ -181,12 +189,91 @@ pub fn install_time_option_keys() -> Vec { impl GoBackend { pub fn from_arg(ba: BackendArg) -> Self { - Self { ba: Arc::new(ba) } + Self { + ba: Arc::new(ba), + module_versions_cache: Default::default(), + } + } + + async fn fetch_go_module_versions( + &self, + config: &Arc, + mod_path: &str, + ) -> eyre::Result>> { + let cache = self + .module_versions_cache + .entry(mod_path.to_string()) + .or_insert_with(|| { + let filename = format!("{}.msgpack.z", hash_to_str(&mod_path.to_string())); + CacheManagerBuilder::new( + self.ba.cache_path.join("go_module_versions").join(filename), + ) + .with_fresh_duration(Settings::get().fetch_remote_versions_cache()) + .build() + }); + + cache + .get_or_try_init_async(async || { + let raw = match cmd!( + "go", + "list", + "-mod=readonly", + "-m", + "-versions", + "-json", + mod_path + ) + .full_env(self.dependency_env(config).await?) + .read() + { + Ok(raw) => raw, + Err(_) => return Ok(None), + }; + + let mod_info = match serde_json::from_str::(&raw) { + Ok(info) => info, + Err(_) => return Ok(None), + }; + + // remove the leading v from the versions + let versions = mod_info + .versions + .into_iter() + .map(|v| VersionInfo { + version: v.trim_start_matches('v').to_string(), + ..Default::default() + }) + .collect(); + + Ok(Some(versions)) + }) + .await + .cloned() } } #[derive(Debug, serde::Deserialize)] #[serde(rename_all = "PascalCase")] pub struct GoModInfo { + #[serde(default)] versions: Vec, } + +#[cfg(test)] +mod tests { + use super::GoModInfo; + + #[test] + fn parse_go_mod_info_without_versions() { + let raw = r#"{"Path":"github.com/go-kratos/kratos/cmd/kratos/v2"}"#; + let info: GoModInfo = serde_json::from_str(raw).unwrap(); + assert!(info.versions.is_empty()); + } + + #[test] + fn parse_go_mod_info_with_versions() { + let raw = r#"{"Path":"example.com/mod","Versions":["v1.0.0","v1.1.0"]}"#; + let info: GoModInfo = serde_json::from_str(raw).unwrap(); + assert_eq!(info.versions, vec!["v1.0.0", "v1.1.0"]); + } +}