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
4 changes: 2 additions & 2 deletions e2e/cli/test_ls_cache
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ export MISE_USE_VERSIONS_HOST=1
# verify that cache is reused for `mise ls`
# see https://github.com/jdx/mise/discussions/6736

assert_contains "mise -v use bat 2>&1" "GET https://mise-versions.jdx.dev/bat 200 OK"
assert_contains "mise -v use bat 2>&1" "GET https://mise-versions.jdx.dev/bat.toml 200 OK"
touch -t 202001010000 "$MISE_CACHE_DIR/bat/"*
assert_not_contains "mise -v ls bat 2>&1" "GET https://mise-versions.jdx.dev/bat 200 OK"
assert_not_contains "mise -v ls bat 2>&1" "GET https://mise-versions.jdx.dev/bat.toml 200 OK"
62 changes: 46 additions & 16 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::lockfile::PlatformInfo;
use crate::platform::Platform;
use crate::plugins::core::CORE_PLUGINS;
use crate::plugins::{PluginType, VERSION_REGEX};
use crate::registry::{REGISTRY, tool_enabled};
use crate::registry::{REGISTRY, full_to_url, normalize_remote, tool_enabled};
use crate::runtime_symlinks::is_runtime_symlink;
use crate::toolset::outdated_info::OutdatedInfo;
use crate::toolset::{ToolRequest, ToolVersion, Toolset, install_state, is_outdated_version};
Expand Down Expand Up @@ -312,6 +312,10 @@ pub trait Backend: Debug + Send + Sync {
/// List remote versions with additional metadata like created_at timestamps.
/// Results are cached. Backends can override `_list_remote_versions_with_info`
/// to provide timestamp information.
///
/// This method first tries the versions host (mise-versions.jdx.dev) which provides
/// version info with created_at timestamps. If that fails, it falls back to the
/// backend's `_list_remote_versions_with_info` implementation.
async fn list_remote_versions_with_info(
&self,
config: &Arc<Config>,
Expand All @@ -320,25 +324,51 @@ pub trait Backend: Debug + Send + Sync {
let remote_versions = remote_versions.lock().await;
let ba = self.ba().clone();
let id = self.id();

// Check if this is an external plugin with a custom remote - skip versions host if so
let use_versions_host = if let Some(plugin) = self.plugin()
&& let Ok(Some(remote_url)) = plugin.get_remote_url()
{
// Check if remote matches the registry default
let normalized_remote =
normalize_remote(&remote_url).unwrap_or_else(|_| "INVALID_URL".into());
let shorthand_remote = REGISTRY
.get(plugin.name())
.and_then(|rt| rt.backends().first().map(|b| full_to_url(b)))
.unwrap_or_default();
let matches =
normalized_remote == normalize_remote(&shorthand_remote).unwrap_or_default();
if !matches {
trace!(
"Skipping versions host for {} because it has a non-default remote",
ba.short
);
}
matches
} else {
true // Core plugins and plugins without remote URLs can use versions host
};

let versions = remote_versions
.get_or_try_init_async(|| async {
trace!("Listing remote versions for {}", ba.to_string());
// Try versions host first (returns just version strings)
match versions_host::list_versions(&ba).await {
Ok(Some(versions)) => {
return Ok(versions
.into_iter()
.map(|v| VersionInfo {
version: v,
created_at: None,
})
.collect());
}
Ok(None) => {}
Err(e) => {
debug!("Error getting versions from versions host: {:#}", e);
// Try versions host first (now returns VersionInfo with timestamps)
if use_versions_host {
match versions_host::list_versions(&ba.short).await {
Ok(Some(versions)) => {
trace!(
"Got {} versions from versions host for {}",
versions.len(),
ba.to_string()
);
return Ok(versions);
}
Comment thread
cursor[bot] marked this conversation as resolved.
Ok(None) => {}
Err(e) => {
debug!("Error getting versions from versions host: {:#}", e);
}
}
};
}
trace!(
"Calling backend to list remote versions for {}",
ba.to_string()
Expand Down
24 changes: 18 additions & 6 deletions src/plugins/core/bun.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use crate::lockfile::PlatformInfo;
use crate::toolset::ToolVersion;
use crate::ui::progress_report::SingleReport;
use crate::{
backend::{Backend, GitHubReleaseInfo, ReleaseType, platform_target::PlatformTarget},
backend::{
Backend, GitHubReleaseInfo, ReleaseType, VersionInfo, platform_target::PlatformTarget,
},
config::{Config, Settings},
platform::Platform,
};
Expand Down Expand Up @@ -111,14 +113,24 @@ impl Backend for BunPlugin {
}
}

async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<String>> {
async fn _list_remote_versions_with_info(
&self,
_config: &Arc<Config>,
) -> Result<Vec<VersionInfo>> {
let versions = github::list_releases("oven-sh/bun")
.await?
.into_iter()
.map(|r| r.tag_name)
.filter_map(|v| v.strip_prefix("bun-v").map(|v| v.to_string()))
.unique()
.sorted_by_cached_key(|s| (Versioning::new(s), s.to_string()))
.filter_map(|r| {
r.tag_name
.strip_prefix("bun-v")
.map(|v| (v.to_string(), r.created_at))
})
.unique_by(|(v, _)| v.clone())
.sorted_by_cached_key(|(s, _)| (Versioning::new(s), s.to_string()))
.map(|(version, created_at)| VersionInfo {
version,
created_at: Some(created_at),
})
.collect();
Ok(versions)
}
Expand Down
31 changes: 24 additions & 7 deletions src/plugins/core/elixir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::install_context::InstallContext;
use crate::plugins::VERSION_REGEX;
use crate::toolset::{ToolVersion, Toolset};
use crate::ui::progress_report::SingleReport;
use crate::{backend::Backend, config::Config};
use crate::{backend::Backend, backend::VersionInfo, config::Config};
use crate::{env, file, plugins};
use async_trait::async_trait;
use eyre::Result;
Expand Down Expand Up @@ -89,15 +89,29 @@ impl Backend for ElixirPlugin {
&self.ba
}

async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<String>> {
let versions: Vec<String> = HTTP_FETCH
async fn _list_remote_versions_with_info(
&self,
_config: &Arc<Config>,
) -> Result<Vec<VersionInfo>> {
// Format: "version hash timestamp checksum"
// Example: "v1.17.3 abc123 2024-12-01T00:00:00Z def456"
let versions: Vec<VersionInfo> = HTTP_FETCH
.get_text("https://builds.hex.pm/builds/elixir/builds.txt")
.await?
.lines()
.unique()
.filter_map(|s| s.split_once(' ').map(|(v, _)| v.trim_start_matches('v')))
.filter(|s| regex!(r"^[0-9]+\.[0-9]+\.[0-9]").is_match(s))
.sorted_by_cached_key(|s| {
.filter_map(|s| {
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() >= 3 {
let version = parts[0].trim_start_matches('v');
let timestamp = parts[2]; // Third field is the timestamp
Some((version.to_string(), timestamp.to_string()))
} else {
None
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Elixir parsing may drop versions with fewer fields

The parsing logic now requires at least 3 whitespace-separated fields per line in builds.txt, whereas the old code only required 2 fields (using split_once(' ')). If any lines in the Elixir builds.txt file have only "version hash" format without a timestamp field, those versions will be silently filtered out and not included in the available versions list. The filter_map returns None for lines with parts.len() < 3, effectively dropping those versions.

Fix in Cursor Fix in Web

.filter(|(v, _)| regex!(r"^[0-9]+\.[0-9]+\.[0-9]").is_match(v))
.sorted_by_cached_key(|(s, _)| {
(
Versioning::new(s.split_once('-').map(|(v, _)| v).unwrap_or(s)),
!VERSION_REGEX.is_match(s),
Expand All @@ -106,7 +120,10 @@ impl Backend for ElixirPlugin {
s.to_string(),
)
})
.map(|s| s.to_string())
.map(|(version, created_at)| VersionInfo {
version,
created_at: Some(created_at),
})
.collect();
Ok(versions)
}
Expand Down
21 changes: 18 additions & 3 deletions src/plugins/core/erlang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::BTreeMap;
use std::{path::PathBuf, sync::Arc};

use crate::backend::Backend;
use crate::backend::VersionInfo;
use crate::backend::platform_target::PlatformTarget;
use crate::cli::args::BackendArg;
use crate::config::{Config, Settings};
Expand Down Expand Up @@ -305,12 +306,23 @@ impl Backend for ErlangPlugin {
&self.ba
}

async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<String>> {
async fn _list_remote_versions_with_info(
&self,
_config: &Arc<Config>,
) -> Result<Vec<VersionInfo>> {
let versions = if Settings::get().erlang.compile == Some(false) {
github::list_releases("erlef/otp_builds")
.await?
.into_iter()
.filter_map(|r| r.tag_name.strip_prefix("OTP-").map(|s| s.to_string()))
.filter_map(|r| {
r.tag_name
.strip_prefix("OTP-")
.map(|s| (s.to_string(), Some(r.created_at)))
})
.map(|(version, created_at)| VersionInfo {
version,
created_at,
})
.collect()
} else {
self.update_kerl().await?;
Expand All @@ -321,7 +333,10 @@ impl Backend for ErlangPlugin {
let versions = output
.split('\n')
.filter(|s| regex!(r"^[0-9].+$").is_match(s))
.map(|s| s.to_string())
.map(|s| VersionInfo {
version: s.to_string(),
created_at: None,
})
.collect();
Ok(versions)
})?
Expand Down
13 changes: 11 additions & 2 deletions src/plugins/core/node.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::backend::VersionInfo;
use crate::backend::static_helpers::fetch_checksum_from_shasums;
use crate::backend::{Backend, VersionCacheManager, platform_target::PlatformTarget};
use crate::build_time::built_info;
Expand Down Expand Up @@ -403,7 +404,10 @@ impl Backend for NodePlugin {
&self.ba
}

async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<String>> {
async fn _list_remote_versions_with_info(
&self,
_config: &Arc<Config>,
) -> Result<Vec<VersionInfo>> {
let settings = Settings::get();
let base = Settings::get().node.mirror_url();
let versions = HTTP_FETCH
Expand All @@ -420,10 +424,14 @@ impl Backend for NodePlugin {
}
})
.map(|v| {
if regex!(r"^v\d+\.").is_match(&v.version) {
let version = if regex!(r"^v\d+\.").is_match(&v.version) {
v.version.strip_prefix('v').unwrap().to_string()
} else {
v.version
};
VersionInfo {
version,
created_at: v.date,
}
})
.rev()
Expand Down Expand Up @@ -792,5 +800,6 @@ fn slug(v: &str) -> String {
#[derive(Debug, Deserialize)]
struct NodeVersion {
version: String,
date: Option<String>,
files: Vec<String>,
}
9 changes: 0 additions & 9 deletions src/plugins/core/ruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,15 +592,6 @@ impl Backend for RubyPlugin {
fn ba(&self) -> &Arc<BackendArg> {
&self.ba
}
async fn _list_remote_versions(&self, config: &Arc<Config>) -> Result<Vec<String>> {
Ok(self
._list_remote_versions_with_info(config)
.await?
.into_iter()
.map(|v| v.version)
.collect())
}

async fn _list_remote_versions_with_info(
&self,
_config: &Arc<Config>,
Expand Down
28 changes: 24 additions & 4 deletions src/plugins/core/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
use std::{collections::BTreeMap, sync::Arc};

use crate::backend::Backend;
use crate::backend::VersionInfo;
use crate::build_time::TARGET;
use crate::cli::args::BackendArg;
use crate::cmd::CmdLineRunner;
Expand Down Expand Up @@ -73,13 +74,32 @@ impl Backend for RustPlugin {
&self.ba
}

async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<String>> {
let versions = github::list_releases("rust-lang/rust")
async fn _list_remote_versions_with_info(
&self,
_config: &Arc<Config>,
) -> Result<Vec<VersionInfo>> {
let versions: Vec<VersionInfo> = github::list_releases("rust-lang/rust")
.await?
.into_iter()
.map(|r| r.tag_name)
.map(|r| VersionInfo {
version: r.tag_name,
created_at: Some(r.created_at),
})
.rev()
.chain(vec!["nightly".into(), "beta".into(), "stable".into()])
.chain(vec![
VersionInfo {
version: "nightly".into(),
created_at: None,
},
VersionInfo {
version: "beta".into(),
created_at: None,
},
VersionInfo {
version: "stable".into(),
created_at: None,
},
])
.collect();
Ok(versions)
}
Expand Down
20 changes: 15 additions & 5 deletions src/plugins/core/swift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::http::HTTP;
use crate::install_context::InstallContext;
use crate::toolset::ToolVersion;
use crate::ui::progress_report::SingleReport;
use crate::{backend::Backend, config::Config};
use crate::{backend::Backend, backend::VersionInfo, config::Config};
use crate::{file, github, gpg, plugins};
use async_trait::async_trait;
use eyre::Result;
Expand Down Expand Up @@ -159,14 +159,24 @@ impl Backend for SwiftPlugin {
&self.ba
}

async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<String>> {
async fn _list_remote_versions_with_info(
&self,
_config: &Arc<Config>,
) -> Result<Vec<VersionInfo>> {
let versions = github::list_releases("swiftlang/swift")
.await?
.into_iter()
.map(|r| r.tag_name)
.filter_map(|v| v.strip_prefix("swift-").map(|v| v.to_string()))
.filter_map(|v| v.strip_suffix("-RELEASE").map(|v| v.to_string()))
.filter_map(|r| {
r.tag_name
.strip_prefix("swift-")
.and_then(|v| v.strip_suffix("-RELEASE"))
.map(|v| (v.to_string(), r.created_at))
})
.rev()
.map(|(version, created_at)| VersionInfo {
version,
created_at: Some(created_at),
})
.collect();
Ok(versions)
}
Expand Down
Loading
Loading