Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion e2e/backend/test_go_install_slow
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ assert_fail go

cat >>.mise.toml <<EOF
[tools]
go = "prefix:1.22"
go = "prefix:1.24"
"go:github.com/golangci/golangci-lint/cmd/golangci-lint" = "latest"
[settings]
experimental = true
Expand All @@ -31,6 +31,9 @@ assert "mise x go:github.com/go-task/task/v3/cmd/task@3.34.1 -- task --version"
# See https://github.com/jdx/mise/discussions/6737
assert "mise x go:github.com/jdx/go-example@e16a340 -- go-example" "hello world"

# Deep sub-module returns no versions from `go list -versions`, so we must keep @latest.
assert_contains "mise x go:github.com/go-kratos/kratos/cmd/kratos/v2@latest -- bash -c 'kratos --help 2>&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
Expand Down
149 changes: 118 additions & 31 deletions src/backend/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -18,6 +21,7 @@ use xx::regex;
#[derive(Debug)]
pub struct GoBackend {
ba: Arc<BackendArg>,
module_versions_cache: DashMap<String, CacheManager<Option<Vec<VersionInfo>>>>,
}

#[async_trait]
Expand Down Expand Up @@ -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::<Vec<_>>();
let module_root_index = if parts[0] == "github.com" {
// Try likely module root index first
Expand All @@ -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::<GoModInfo>(&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![])
Expand All @@ -113,7 +106,7 @@ impl Backend for GoBackend {
async fn install_version_(
&self,
ctx: &InstallContext,
tv: ToolVersion,
mut tv: ToolVersion,
) -> eyre::Result<ToolVersion> {
// Check if go is available
self.warn_if_dependency_missing(
Expand All @@ -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();
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

let opts = self.ba.opts();

let install = async |v| {
Expand All @@ -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)
}
Expand Down Expand Up @@ -181,12 +189,91 @@ pub fn install_time_option_keys() -> Vec<String> {

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<Config>,
mod_path: &str,
) -> eyre::Result<Option<Vec<VersionInfo>>> {
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::<GoModInfo>(&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<String>,
}

#[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"]);
}
}
Loading