diff --git a/src/backend/asdf.rs b/src/backend/asdf.rs index 66b100f4a..e3fd5b95d 100644 --- a/src/backend/asdf.rs +++ b/src/backend/asdf.rs @@ -7,28 +7,24 @@ use std::sync::Arc; use color_eyre::eyre::{eyre, Result, WrapErr}; use console::style; -use itertools::Itertools; use rayon::prelude::*; -use url::Url; use crate::backend::external_plugin_cache::ExternalPluginCache; use crate::backend::{ABackend, Backend, BackendList}; use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::BackendArg; -use crate::config::{Config, Settings, SETTINGS}; +use crate::config::{Config, SETTINGS}; use crate::env_diff::{EnvDiff, EnvDiffOperation}; use crate::git::Git; use crate::hash::hash_to_str; -use crate::http::HTTP_FETCH; use crate::install_context::InstallContext; use crate::plugins::asdf_plugin::AsdfPlugin; use crate::plugins::mise_plugin_toml::MisePluginToml; use crate::plugins::Script::{Download, ExecEnv, Install, ParseLegacyFile}; use crate::plugins::{Plugin, PluginType, Script, ScriptManager}; -use crate::registry::REGISTRY; use crate::toolset::{ToolRequest, ToolVersion, Toolset}; use crate::ui::progress_report::SingleReport; -use crate::{dirs, env, file, http, registry}; +use crate::{dirs, env, file}; /// This represents a plugin installed to ~/.local/share/mise/plugins pub struct AsdfBackend { @@ -101,56 +97,6 @@ impl AsdfBackend { .collect()) } - fn fetch_versions(&self) -> Result>> { - let settings = Settings::get(); - if !settings.use_versions_host { - return Ok(None); - } - // ensure that we're using a default shorthand plugin - let git = Git::new(&self.plugin_path); - let normalized_remote = normalize_remote(&git.get_remote_url().unwrap_or_default()) - .unwrap_or("INVALID_URL".into()); - let shorthand_remote = REGISTRY - .get(self.name.as_str()) - .map(|s| registry::full_to_url(s)) - .unwrap_or_default(); - if normalized_remote != normalize_remote(&shorthand_remote).unwrap_or_default() { - return Ok(None); - } - let settings = Settings::get(); - let raw_versions = match settings.paranoid { - true => HTTP_FETCH.get_text(format!("https://mise-versions.jdx.dev/{}", self.name)), - false => HTTP_FETCH.get_text(format!("http://mise-versions.jdx.dev/{}", self.name)), - }; - let versions = - // using http is not a security concern and enabling tls makes mise significantly slower - match raw_versions { - Err(err) if http::error_code(&err) == Some(404) => return Ok(None), - res => res?, - }; - let versions = versions - .lines() - .map(|v| v.trim().to_string()) - .filter(|v| !v.is_empty()) - .collect_vec(); - match versions.is_empty() { - true => Ok(None), - false => Ok(Some(versions)), - } - } - - fn fetch_remote_versions(&self) -> Result> { - match self.fetch_versions() { - Ok(Some(versions)) => return Ok(versions), - Err(err) => warn!( - "Failed to fetch remote versions for plugin {}: {}", - style(&self.name).blue().for_stderr(), - err - ), - _ => {} - }; - self.plugin.fetch_remote_versions() - } fn fetch_cached_legacy_file(&self, legacy_file: &Path) -> Result> { let fp = self.legacy_cache_file_path(legacy_file); if !fp.exists() || fp.metadata()?.modified()? < legacy_file.metadata()?.modified()? { @@ -310,7 +256,7 @@ impl Backend for AsdfBackend { fn _list_remote_versions(&self) -> Result> { self.remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) + .get_or_try_init(|| self.plugin.fetch_remote_versions()) .wrap_err_with(|| { eyre!( "Failed listing remote versions for plugin {}", @@ -470,13 +416,6 @@ impl Debug for AsdfBackend { } } -fn normalize_remote(remote: &str) -> eyre::Result { - let url = Url::parse(remote)?; - let host = url.host_str().unwrap(); - let path = url.path().trim_end_matches(".git"); - Ok(format!("{host}{path}")) -} - #[cfg(test)] mod tests { use test_log::test; diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 3a2880172..b98fdc8ba 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::{Debug, Display, Formatter}; use std::fs::File; use std::hash::Hash; @@ -9,12 +9,14 @@ use console::style; use contracts::requires; use eyre::{bail, eyre, WrapErr}; use itertools::Itertools; +use once_cell::sync::Lazy; use rayon::prelude::*; use regex::Regex; use strum::IntoEnumIterator; use versions::Versioning; use self::backend_meta::BackendMeta; +use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::{BackendArg, ToolVersionType}; use crate::cmd::CmdLineRunner; use crate::config::{Config, CONFIG, SETTINGS}; @@ -25,7 +27,7 @@ use crate::plugins::{Plugin, PluginType, VERSION_REGEX}; use crate::runtime_symlinks::is_runtime_symlink; use crate::toolset::{is_outdated_version, ToolRequest, ToolVersion, Toolset}; use crate::ui::progress_report::SingleReport; -use crate::{dirs, env, file, lock_file}; +use crate::{dirs, env, file, lock_file, versions_host}; pub mod asdf; pub mod backend_meta; @@ -41,6 +43,7 @@ pub mod vfox; pub type ABackend = Arc; pub type BackendMap = BTreeMap; pub type BackendList = Vec; +pub type VersionCacheManager = CacheManager>; #[derive( Debug, @@ -196,24 +199,40 @@ pub trait Backend: Debug + Send + Sync { } Ok(deps) } + fn list_remote_versions(&self) -> eyre::Result> { self.ensure_dependencies_installed()?; - trace!("Listing remote versions for {}", self.fa().to_string()); - let versions = self - ._list_remote_versions()? - .into_iter() - .filter(|v| match v.parse::() { - Ok(ToolVersionType::Version(_)) => true, - _ => { - warn!("Invalid version: {}@{v}", self.id()); - false + self.get_remote_version_cache() + .get_or_try_init(|| { + trace!("Listing remote versions for {}", self.fa().to_string()); + match versions_host::list_versions(self.fa()) { + Ok(Some(versions)) => return Ok(versions), + Ok(None) => {} + Err(e) => { + debug!("Error getting versions from versions host: {:#}", e); + } + }; + trace!( + "Calling backend to list remote versions for {}", + self.fa().to_string() + ); + let versions = self + ._list_remote_versions()? + .into_iter() + .filter(|v| match v.parse::() { + Ok(ToolVersionType::Version(_)) => true, + _ => { + warn!("Invalid version: {}@{v}", self.id()); + false + } + }) + .collect_vec(); + if versions.is_empty() { + warn!("No versions found for {}", self.id()); } + Ok(versions) }) - .collect_vec(); - if versions.is_empty() { - warn!("No versions found for {}", self.id()); - } - Ok(versions) + .cloned() } fn _list_remote_versions(&self) -> eyre::Result>; fn latest_stable_version(&self) -> eyre::Result> { @@ -556,6 +575,30 @@ pub trait Backend: Debug + Send + Sync { .collect(); Ok(versions) } + + fn get_remote_version_cache(&self) -> Arc { + static REMOTE_VERSION_CACHE: Lazy>>> = + Lazy::new(|| Mutex::new(HashMap::new())); + + REMOTE_VERSION_CACHE + .lock() + .unwrap() + .entry(self.fa().full.to_string()) + .or_insert_with(|| { + let mut cm = CacheManagerBuilder::new( + self.fa().cache_path.join("remote_versions.msgpack.z"), + ) + .with_fresh_duration(SETTINGS.fetch_remote_versions_cache()); + if let Some(plugin_path) = self.plugin().map(|p| p.path()) { + cm = cm + .with_fresh_file(plugin_path.clone()) + .with_fresh_file(plugin_path.join("bin/list-all")) + } + + Arc::new(cm.build()) + }) + .clone() + } } fn find_match_in_list(list: &[String], query: &str) -> Option { diff --git a/src/backend/ubi.rs b/src/backend/ubi.rs index ae6589707..d4affae28 100644 --- a/src/backend/ubi.rs +++ b/src/backend/ubi.rs @@ -1,8 +1,5 @@ use std::fmt::Debug; -use eyre::eyre; -use ubi::UbiBuilder; - use crate::backend::{Backend, BackendType}; use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::BackendArg; @@ -11,6 +8,9 @@ use crate::env::GITHUB_TOKEN; use crate::install_context::InstallContext; use crate::toolset::ToolRequest; use crate::{github, http}; +use eyre::eyre; +use ubi::UbiBuilder; +use xx::regex; #[derive(Debug)] pub struct UbiBackend { @@ -41,6 +41,11 @@ impl Backend for UbiBackend { Ok(github::list_releases(self.name())? .into_iter() .map(|r| r.tag_name) + // trim 'v' prefixes if they exist + .map(|t| match regex!(r"^v[0-9]").is_match(&t) { + true => t[1..].to_string(), + false => t, + }) .rev() .collect()) }) diff --git a/src/main.rs b/src/main.rs index 2aa8cfeea..3f2cb41c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,7 @@ pub(crate) mod timeout; mod toml; mod toolset; mod ui; +mod versions_host; pub use crate::exit::exit; diff --git a/src/plugins/asdf_plugin.rs b/src/plugins/asdf_plugin.rs index 38fceaf76..bef1552c9 100644 --- a/src/plugins/asdf_plugin.rs +++ b/src/plugins/asdf_plugin.rs @@ -213,6 +213,10 @@ impl Plugin for AsdfPlugin { &self.name } + fn path(&self) -> PathBuf { + self.plugin_path.clone() + } + fn get_plugin_type(&self) -> PluginType { PluginType::Asdf } diff --git a/src/plugins/core/bun.rs b/src/plugins/core/bun.rs index d8a528eed..80df1c1c1 100644 --- a/src/plugins/core/bun.rs +++ b/src/plugins/core/bun.rs @@ -28,24 +28,6 @@ impl BunPlugin { Self { core } } - fn fetch_remote_versions(&self) -> Result> { - match self.core.fetch_remote_versions_from_mise() { - Ok(Some(versions)) => return Ok(versions), - Ok(None) => {} - Err(e) => warn!("failed to fetch remote versions: {}", e), - } - let releases: Vec = - HTTP_FETCH.json("https://api.github.com/repos/oven-sh/bun/releases?per_page=100")?; - let versions = releases - .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())) - .collect(); - Ok(versions) - } - fn bun_bin(&self, tv: &ToolVersion) -> PathBuf { tv.install_path().join("bin/bun") } @@ -103,10 +85,16 @@ impl Backend for BunPlugin { } fn _list_remote_versions(&self) -> Result> { - self.core - .remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) - .cloned() + let releases: Vec = + HTTP_FETCH.json("https://api.github.com/repos/oven-sh/bun/releases?per_page=100")?; + let versions = releases + .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())) + .collect(); + Ok(versions) } fn legacy_filenames(&self) -> Result> { diff --git a/src/plugins/core/deno.rs b/src/plugins/core/deno.rs index fd2af94aa..f1b9ff516 100644 --- a/src/plugins/core/deno.rs +++ b/src/plugins/core/deno.rs @@ -30,24 +30,6 @@ impl DenoPlugin { Self { core } } - fn fetch_remote_versions(&self) -> Result> { - match self.core.fetch_remote_versions_from_mise() { - Ok(Some(versions)) => return Ok(versions), - Ok(None) => {} - Err(e) => warn!("failed to fetch remote versions: {}", e), - } - let versions: DenoVersions = HTTP_FETCH.json("https://deno.com/versions.json")?; - let versions = versions - .cli - .into_iter() - .filter(|v| v.starts_with('v')) - .map(|v| v.trim_start_matches('v').to_string()) - .unique() - .sorted_by_cached_key(|s| (Versioning::new(s), s.to_string())) - .collect(); - Ok(versions) - } - fn deno_bin(&self, tv: &ToolVersion) -> PathBuf { tv.install_path().join(if cfg!(target_os = "windows") { "bin/deno.exe" @@ -111,10 +93,16 @@ impl Backend for DenoPlugin { } fn _list_remote_versions(&self) -> Result> { - self.core - .remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) - .cloned() + let versions: DenoVersions = HTTP_FETCH.json("https://deno.com/versions.json")?; + let versions = versions + .cli + .into_iter() + .filter(|v| v.starts_with('v')) + .map(|v| v.trim_start_matches('v').to_string()) + .unique() + .sorted_by_cached_key(|s| (Versioning::new(s), s.to_string())) + .collect(); + Ok(versions) } fn legacy_filenames(&self) -> Result> { diff --git a/src/plugins/core/erlang.rs b/src/plugins/core/erlang.rs index e92f123fd..45a70081d 100644 --- a/src/plugins/core/erlang.rs +++ b/src/plugins/core/erlang.rs @@ -69,13 +69,13 @@ impl ErlangPlugin { file::make_executable(self.kerl_path())?; Ok(()) } +} - fn fetch_remote_versions(&self) -> Result> { - match self.core.fetch_remote_versions_from_mise() { - Ok(Some(versions)) => return Ok(versions), - Ok(None) => {} - Err(e) => warn!("failed to fetch remote versions: {}", e), - } +impl Backend for ErlangPlugin { + fn fa(&self) -> &BackendArg { + &self.core.fa + } + fn _list_remote_versions(&self) -> Result> { self.update_kerl()?; let versions = CorePlugin::run_fetch_task_with_timeout(move || { let output = cmd!(self.kerl_path(), "list", "releases", "all") @@ -90,18 +90,6 @@ impl ErlangPlugin { })?; Ok(versions) } -} - -impl Backend for ErlangPlugin { - fn fa(&self) -> &BackendArg { - &self.core.fa - } - fn _list_remote_versions(&self) -> Result> { - self.core - .remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) - .cloned() - } fn install_version_impl(&self, ctx: &InstallContext) -> Result<()> { self.update_kerl()?; diff --git a/src/plugins/core/go.rs b/src/plugins/core/go.rs index 28ee63c1c..b8a5a76be 100644 --- a/src/plugins/core/go.rs +++ b/src/plugins/core/go.rs @@ -6,7 +6,7 @@ use crate::backend::Backend; use crate::cli::args::BackendArg; use crate::cli::version::{ARCH, OS}; use crate::cmd::CmdLineRunner; -use crate::config::{Config, Settings}; +use crate::config::{Config, Settings, SETTINGS}; use crate::http::HTTP; use crate::install_context::InstallContext; use crate::plugins::core::CorePlugin; @@ -30,26 +30,6 @@ impl GoPlugin { } } - fn fetch_remote_versions(&self) -> eyre::Result> { - match self.core.fetch_remote_versions_from_mise() { - Ok(Some(versions)) => return Ok(versions), - Ok(None) => {} - Err(e) => warn!("failed to fetch remote versions: {}", e), - } - let settings = Settings::get(); - CorePlugin::run_fetch_task_with_timeout(move || { - let output = cmd!("git", "ls-remote", "--tags", &settings.go_repo, "go*").read()?; - let lines = output.split('\n'); - let versions = lines.map(|s| s.split("/go").last().unwrap_or_default().to_string()) - .filter(|s| !s.is_empty()) - .filter(|s| !regex!(r"^1($|\.0|\.0\.[0-9]|\.1|\.1rc[0-9]|\.1\.[0-9]|.2|\.2rc[0-9]|\.2\.1|.8.5rc5)$").is_match(s)) - .unique() - .sorted_by_cached_key(|s| (Versioning::new(s), s.to_string())) - .collect(); - Ok(versions) - }) - } - // Represents go binary path fn go_bin(&self, tv: &ToolVersion) -> PathBuf { tv.install_path().join("bin/go") @@ -190,10 +170,17 @@ impl Backend for GoPlugin { &self.core.fa } fn _list_remote_versions(&self) -> eyre::Result> { - self.core - .remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) - .cloned() + CorePlugin::run_fetch_task_with_timeout(move || { + let output = cmd!("git", "ls-remote", "--tags", &SETTINGS.go_repo, "go*").read()?; + let lines = output.split('\n'); + let versions = lines.map(|s| s.split("/go").last().unwrap_or_default().to_string()) + .filter(|s| !s.is_empty()) + .filter(|s| !regex!(r"^1($|\.0|\.0\.[0-9]|\.1|\.1rc[0-9]|\.1\.[0-9]|.2|\.2rc[0-9]|\.2\.1|.8.5rc5)$").is_match(s)) + .unique() + .sorted_by_cached_key(|s| (Versioning::new(s), s.to_string())) + .collect(); + Ok(versions) + }) } fn legacy_filenames(&self) -> eyre::Result> { Ok(vec![".go-version".into()]) diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 567713387..eed5cefbc 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -77,42 +77,6 @@ impl JavaPlugin { }) } - fn fetch_remote_versions(&self) -> Result> { - // TODO: find out how to get this to work for different os/arch - // See https://github.com/jdx/mise/issues/1196 - // match self.core.fetch_remote_versions_from_mise() { - // Ok(Some(versions)) => return Ok(versions), - // Ok(None) => {} - // Err(e) => warn!("failed to fetch remote versions: {}", e), - // } - let versions = self - .fetch_java_metadata("ga")? - .iter() - .sorted_by_cached_key(|(v, m)| { - let is_shorthand = regex!(r"^\d").is_match(v); - let vendor = &m.vendor; - let is_jdk = m - .image_type - .as_ref() - .is_some_and(|image_type| image_type == "jdk"); - let features = 10 - m.features.len(); - let version = Versioning::new(v); - ( - is_shorthand, - vendor, - is_jdk, - features, - version, - v.to_string(), - ) - }) - .map(|(v, _)| v.clone()) - .unique() - .collect(); - - Ok(versions) - } - fn java_bin(&self, tv: &ToolVersion) -> PathBuf { tv.install_path().join("bin/java") } @@ -306,10 +270,39 @@ impl Backend for JavaPlugin { } fn _list_remote_versions(&self) -> Result> { - self.core - .remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) - .cloned() + // TODO: find out how to get this to work for different os/arch + // See https://github.com/jdx/mise/issues/1196 + // match self.core.fetch_remote_versions_from_mise() { + // Ok(Some(versions)) => return Ok(versions), + // Ok(None) => {} + // Err(e) => warn!("failed to fetch remote versions: {}", e), + // } + let versions = self + .fetch_java_metadata("ga")? + .iter() + .sorted_by_cached_key(|(v, m)| { + let is_shorthand = regex!(r"^\d").is_match(v); + let vendor = &m.vendor; + let is_jdk = m + .image_type + .as_ref() + .is_some_and(|image_type| image_type == "jdk"); + let features = 10 - m.features.len(); + let version = Versioning::new(v); + ( + is_shorthand, + vendor, + is_jdk, + features, + version, + v.to_string(), + ) + }) + .map(|(v, _)| v.clone()) + .unique() + .collect(); + + Ok(versions) } fn list_installed_versions_matching(&self, query: &str) -> eyre::Result> { diff --git a/src/plugins/core/mod.rs b/src/plugins/core/mod.rs index 77c8866f0..0b5b94e5f 100644 --- a/src/plugins/core/mod.rs +++ b/src/plugins/core/mod.rs @@ -1,18 +1,15 @@ use eyre::Result; -use itertools::Itertools; use once_cell::sync::Lazy; use std::ffi::OsString; +use std::path::PathBuf; use std::sync::Arc; pub use python::PythonPlugin; use crate::backend::{Backend, BackendMap}; -use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::BackendArg; use crate::config::{Settings, SETTINGS}; -use crate::env; use crate::env::PATH_KEY; -use crate::http::HTTP_FETCH; #[cfg(unix)] use crate::plugins::core::bun::BunPlugin; use crate::plugins::core::deno::DenoPlugin; @@ -31,6 +28,7 @@ use crate::plugins::core::zig::ZigPlugin; use crate::plugins::{Plugin, PluginList, PluginType}; use crate::timeout::run_with_timeout; use crate::toolset::ToolVersion; +use crate::{dirs, env}; #[cfg(unix)] mod bun; @@ -85,10 +83,10 @@ pub static CORE_PLUGINS: Lazy = Lazy::new(|| { .collect() }); +// TODO: remove this struct #[derive(Debug)] pub struct CorePlugin { pub fa: BackendArg, - pub remote_version_cache: CacheManager>, } impl CorePlugin { @@ -102,16 +100,7 @@ impl CorePlugin { } pub fn new(fa: BackendArg) -> Self { - Self { - remote_version_cache: CacheManagerBuilder::new( - fa.cache_path.join("remote_versions.msgpack.z"), - ) - .with_fresh_duration(SETTINGS.fetch_remote_versions_cache()) - .with_cache_key(SETTINGS.node.mirror_url.clone().unwrap_or_default()) - .with_cache_key(SETTINGS.node.flavor.clone().unwrap_or_default()) - .build(), - fa, - } + Self { fa } } pub fn path_env_with_tv_path(tv: &ToolVersion) -> Result { @@ -127,31 +116,18 @@ impl CorePlugin { { run_with_timeout(f, SETTINGS.fetch_remote_versions_timeout()) } - - pub fn fetch_remote_versions_from_mise(&self) -> Result>> { - let settings = Settings::get(); - if !settings.use_versions_host { - return Ok(None); - } - // using http is not a security concern and enabling tls makes mise significantly slower - let raw = match settings.paranoid { - true => HTTP_FETCH.get_text(format!("https://mise-versions.jdx.dev/{}", &self.fa.name)), - false => HTTP_FETCH.get_text(format!("http://mise-versions.jdx.dev/{}", &self.fa.name)), - }?; - let versions = raw - .lines() - .map(|v| v.trim().to_string()) - .filter(|v| !v.is_empty()) - .collect_vec(); - Ok(Some(versions)) - } } +// TODO: remove this since core "plugins" are not plugins, this is legacy from when it was just asdf/core impl Plugin for CorePlugin { fn name(&self) -> &str { &self.fa.name } + fn path(&self) -> PathBuf { + dirs::PLUGINS.join(self.name()) + } + fn get_plugin_type(&self) -> PluginType { PluginType::Core } diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index 9426cc141..cd06c5c4b 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -1,11 +1,9 @@ -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; - -use crate::backend::Backend; +use crate::backend::{Backend, VersionCacheManager}; use crate::build_time::built_info; +use crate::cache::CacheManagerBuilder; use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; -use crate::config::settings::{DEFAULT_NODE_MIRROR_URL, SETTINGS}; +use crate::config::settings::SETTINGS; use crate::config::{Config, Settings}; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; @@ -15,6 +13,9 @@ use crate::ui::progress_report::SingleReport; use crate::{env, file, hash, http}; use eyre::{bail, ensure, Result}; use serde_derive::Deserialize; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, OnceLock}; use tempfile::tempdir_in; use url::Url; use xx::regex; @@ -31,44 +32,6 @@ impl NodePlugin { } } - fn fetch_remote_versions(&self) -> Result> { - let settings = Settings::get(); - let node_url_overridden = settings.node.mirror_url().as_str() != DEFAULT_NODE_MIRROR_URL; - if !node_url_overridden { - match self.core.fetch_remote_versions_from_mise() { - Ok(Some(versions)) => return Ok(versions), - Ok(None) => {} - Err(e) => warn!("failed to fetch remote versions: {}", e), - } - } - self.fetch_remote_versions_from_node(&settings.node.mirror_url()) - } - fn fetch_remote_versions_from_node(&self, base: &Url) -> Result> { - let settings = Settings::get(); - let versions = HTTP_FETCH - .json::, _>(base.join("index.json")?)? - .into_iter() - .filter(|v| { - if let Some(flavor) = &settings.node.flavor { - v.files - .iter() - .any(|f| f == &format!("{}-{}-{}", os(), arch(), flavor)) - } else { - true - } - }) - .map(|v| { - if regex!(r"^v\d+\.").is_match(&v.version) { - v.version.strip_prefix('v').unwrap().to_string() - } else { - v.version - } - }) - .rev() - .collect(); - Ok(versions) - } - fn install_precompiled(&self, ctx: &InstallContext, opts: &BuildOpts) -> Result<()> { let settings = Settings::get(); match self.fetch_tarball( @@ -294,11 +257,44 @@ impl Backend for NodePlugin { &self.core.fa } + fn get_remote_version_cache(&self) -> Arc { + static CACHE: OnceLock> = OnceLock::new(); + CACHE + .get_or_init(|| { + CacheManagerBuilder::new(self.fa().cache_path.join("remote_versions.msgpack.z")) + .with_fresh_duration(SETTINGS.fetch_remote_versions_cache()) + .with_cache_key(SETTINGS.node.mirror_url.clone().unwrap_or_default()) + .with_cache_key(SETTINGS.node.flavor.clone().unwrap_or_default()) + .build() + .into() + }) + .clone() + } + fn _list_remote_versions(&self) -> Result> { - self.core - .remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) - .cloned() + let base = SETTINGS.node.mirror_url(); + let versions = HTTP_FETCH + .json::, _>(base.join("index.json")?)? + .into_iter() + .filter(|v| { + if let Some(flavor) = &SETTINGS.node.flavor { + v.files + .iter() + .any(|f| f == &format!("{}-{}-{}", os(), arch(), flavor)) + } else { + true + } + }) + .map(|v| { + if regex!(r"^v\d+\.").is_match(&v.version) { + v.version.strip_prefix('v').unwrap().to_string() + } else { + v.version + } + }) + .rev() + .collect(); + Ok(versions) } fn get_aliases(&self) -> Result> { diff --git a/src/plugins/core/python.rs b/src/plugins/core/python.rs index fbb04174f..75d1c5009 100644 --- a/src/plugins/core/python.rs +++ b/src/plugins/core/python.rs @@ -1,4 +1,4 @@ -use crate::backend::Backend; +use crate::backend::{Backend, VersionCacheManager}; use crate::build_time::built_info; use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::BackendArg; @@ -16,6 +16,7 @@ use eyre::{bail, eyre}; use itertools::Itertools; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use std::sync::{Arc, OnceLock}; use versions::Versioning; use xx::regex; @@ -76,27 +77,6 @@ impl PythonPlugin { Ok(()) } - fn fetch_remote_versions(&self) -> eyre::Result> { - match self.core.fetch_remote_versions_from_mise() { - Ok(Some(versions)) => return Ok(versions), - Ok(None) => {} - Err(e) => warn!("failed to fetch remote versions: {}", e), - } - self.install_or_update_python_build()?; - let python_build_bin = self.python_build_bin(); - CorePlugin::run_fetch_task_with_timeout(move || { - let output = cmd!(python_build_bin, "--definitions").read()?; - let versions = output - .split('\n') - // remove free-threaded pythons like 3.13t and 3.14t-dev - .filter(|s| !regex!(r"\dt(-dev)?$").is_match(s)) - .map(|s| s.to_string()) - .sorted_by_cached_key(|v| regex!(r"^\d+").is_match(v)) - .collect(); - Ok(versions) - }) - } - fn python_path(&self, tv: &ToolVersion) -> PathBuf { if cfg!(windows) { tv.install_path().join("python.exe") @@ -365,13 +345,35 @@ impl Backend for PythonPlugin { .map(|(v, _, _)| v.clone()) .collect()) } else { - self.core - .remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) - .cloned() + self.install_or_update_python_build()?; + let python_build_bin = self.python_build_bin(); + CorePlugin::run_fetch_task_with_timeout(move || { + let output = cmd!(python_build_bin, "--definitions").read()?; + let versions = output + .split('\n') + // remove free-threaded pythons like 3.13t and 3.14t-dev + .filter(|s| !regex!(r"\dt(-dev)?$").is_match(s)) + .map(|s| s.to_string()) + .sorted_by_cached_key(|v| regex!(r"^\d+").is_match(v)) + .collect(); + Ok(versions) + }) } } + fn get_remote_version_cache(&self) -> Arc { + static CACHE: OnceLock> = OnceLock::new(); + CACHE + .get_or_init(|| { + CacheManagerBuilder::new(self.fa().cache_path.join("remote_versions.msgpack.z")) + .with_fresh_duration(SETTINGS.fetch_remote_versions_cache()) + .with_cache_key((SETTINGS.python.compile == Some(false)).to_string()) + .build() + .into() + }) + .clone() + } + #[cfg(windows)] fn list_bin_paths(&self, tv: &ToolVersion) -> eyre::Result> { Ok(vec![tv.install_path()]) diff --git a/src/plugins/core/ruby.rs b/src/plugins/core/ruby.rs index 8ee9ee271..f21936b46 100644 --- a/src/plugins/core/ruby.rs +++ b/src/plugins/core/ruby.rs @@ -156,24 +156,6 @@ impl RubyPlugin { Ok(updated_at < DAILY) } - fn fetch_remote_versions(&self) -> Result> { - match self.core.fetch_remote_versions_from_mise() { - Ok(Some(versions)) => return Ok(versions), - Ok(None) => {} - Err(e) => warn!("failed to fetch remote versions: {}", e), - } - if let Err(err) = self.update_build_tool() { - warn!("{err}"); - } - let ruby_build_bin = self.ruby_build_bin(); - let versions = CorePlugin::run_fetch_task_with_timeout(move || { - let output = cmd!(ruby_build_bin, "--definitions").read()?; - let versions = output.split('\n').map(|s| s.to_string()).collect(); - Ok(versions) - })?; - Ok(versions) - } - fn ruby_path(&self, tv: &ToolVersion) -> PathBuf { tv.install_path().join("bin/ruby") } @@ -346,10 +328,16 @@ impl Backend for RubyPlugin { &self.core.fa } fn _list_remote_versions(&self) -> Result> { - self.core - .remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) - .cloned() + if let Err(err) = self.update_build_tool() { + warn!("{err}"); + } + let ruby_build_bin = self.ruby_build_bin(); + let versions = CorePlugin::run_fetch_task_with_timeout(move || { + let output = cmd!(ruby_build_bin, "--definitions").read()?; + let versions = output.split('\n').map(|s| s.to_string()).collect(); + Ok(versions) + })?; + Ok(versions) } fn legacy_filenames(&self) -> Result> { diff --git a/src/plugins/core/ruby_windows.rs b/src/plugins/core/ruby_windows.rs index fc47656cb..d50266126 100644 --- a/src/plugins/core/ruby_windows.rs +++ b/src/plugins/core/ruby_windows.rs @@ -31,29 +31,6 @@ impl RubyPlugin { } } - fn fetch_remote_versions(&self) -> Result> { - // TODO: use windows set of versions - // match self.core.fetch_remote_versions_from_mise() { - // Ok(Some(versions)) => return Ok(versions), - // Ok(None) => {} - // Err(e) => warn!("failed to fetch remote versions: {}", e), - // } - let releases: Vec = github::list_releases("oneclick/rubyinstaller2")?; - let versions = releases - .into_iter() - .map(|r| r.tag_name) - .filter_map(|v| { - regex!(r"RubyInstaller-([0-9.]+)-.*") - .replace(&v, "$1") - .parse() - .ok() - }) - .unique() - .sorted_by_cached_key(|s: &String| (Versioning::new(s), s.to_string())) - .collect(); - Ok(versions) - } - fn ruby_path(&self, tv: &ToolVersion) -> PathBuf { tv.install_path().join("bin").join("ruby.exe") } @@ -164,10 +141,26 @@ impl Backend for RubyPlugin { &self.core.fa } fn _list_remote_versions(&self) -> Result> { - self.core - .remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) - .cloned() + // TODO: use windows set of versions + // match self.core.fetch_remote_versions_from_mise() { + // Ok(Some(versions)) => return Ok(versions), + // Ok(None) => {} + // Err(e) => warn!("failed to fetch remote versions: {}", e), + // } + let releases: Vec = github::list_releases("oneclick/rubyinstaller2")?; + let versions = releases + .into_iter() + .map(|r| r.tag_name) + .filter_map(|v| { + regex!(r"RubyInstaller-([0-9.]+)-.*") + .replace(&v, "$1") + .parse() + .ok() + }) + .unique() + .sorted_by_cached_key(|s: &String| (Versioning::new(s), s.to_string())) + .collect(); + Ok(versions) } fn legacy_filenames(&self) -> Result> { diff --git a/src/plugins/core/zig.rs b/src/plugins/core/zig.rs index 62d09afcb..2c7eff321 100644 --- a/src/plugins/core/zig.rs +++ b/src/plugins/core/zig.rs @@ -40,26 +40,6 @@ impl ZigPlugin { .execute() } - fn fetch_remote_versions(&self) -> Result> { - match self.core.fetch_remote_versions_from_mise() { - Ok(Some(versions)) => { - return Ok(versions); - } - Ok(None) => {} - Err(e) => warn!("failed to fetch remote versions: {}", e), - } - - let releases: Vec = - HTTP_FETCH.json("https://api.github.com/repos/ziglang/zig/releases?per_page=100")?; - let versions = releases - .into_iter() - .map(|r| r.tag_name) - .unique() - .sorted_by_cached_key(|s| (Versioning::new(s), s.to_string())) - .collect(); - Ok(versions) - } - fn download(&self, tv: &ToolVersion, pr: &dyn SingleReport) -> Result { let url = if tv.version == "ref:master" { format!( @@ -142,10 +122,15 @@ impl Backend for ZigPlugin { } fn _list_remote_versions(&self) -> Result> { - self.core - .remote_version_cache - .get_or_try_init(|| self.fetch_remote_versions()) - .cloned() + let releases: Vec = + HTTP_FETCH.json("https://api.github.com/repos/ziglang/zig/releases?per_page=100")?; + let versions = releases + .into_iter() + .map(|r| r.tag_name) + .unique() + .sorted_by_cached_key(|s| (Versioning::new(s), s.to_string())) + .collect(); + Ok(versions) } fn legacy_filenames(&self) -> Result> { diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index d6436b463..a510bf4e3 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -14,6 +14,7 @@ use regex::Regex; pub use script_manager::{Script, ScriptManager}; use std::collections::BTreeMap; use std::fmt::{Debug, Display}; +use std::path::PathBuf; use std::vec; pub mod asdf_plugin; @@ -84,6 +85,7 @@ pub type PluginList = Vec; pub trait Plugin: Debug + Send { fn name(&self) -> &str; + fn path(&self) -> PathBuf; fn get_plugin_type(&self) -> PluginType; fn get_remote_url(&self) -> eyre::Result>; fn current_abbrev_ref(&self) -> eyre::Result>; diff --git a/src/plugins/vfox_plugin.rs b/src/plugins/vfox_plugin.rs index 7fd393be1..19d87355f 100644 --- a/src/plugins/vfox_plugin.rs +++ b/src/plugins/vfox_plugin.rs @@ -84,6 +84,10 @@ impl Plugin for VfoxPlugin { &self.name } + fn path(&self) -> PathBuf { + self.plugin_path.clone() + } + fn get_plugin_type(&self) -> PluginType { PluginType::Vfox } diff --git a/src/versions_host.rs b/src/versions_host.rs new file mode 100644 index 000000000..2756f0266 --- /dev/null +++ b/src/versions_host.rs @@ -0,0 +1,60 @@ +use crate::cli::args::BackendArg; +use crate::config::SETTINGS; +use crate::http::HTTP_FETCH; +use crate::registry::REGISTRY; +use crate::{backend, http, registry}; +use url::Url; + +pub fn list_versions(ba: &BackendArg) -> eyre::Result>> { + if !SETTINGS.use_versions_host || ba.short.contains(':') { + return Ok(None); + } + // ensure that we're using a default shorthand plugin + if let Some(plugin) = backend::get(ba).plugin() { + if let Ok(Some(remote_url)) = plugin.get_remote_url() { + let normalized_remote = normalize_remote(&remote_url).unwrap_or("INVALID_URL".into()); + let shorthand_remote = REGISTRY + .get(plugin.name()) + .map(|s| registry::full_to_url(s)) + .unwrap_or_default(); + if normalized_remote != normalize_remote(&shorthand_remote).unwrap_or_default() { + trace!( + "Skipping versions host check for {} because it has a non-default remote", + ba.short + ); + return Ok(None); + } + } + } + let response = match SETTINGS.paranoid { + true => HTTP_FETCH.get_text(format!("https://mise-versions.jdx.dev/{}", &ba.short)), + false => HTTP_FETCH.get_text(format!("http://mise-versions.jdx.dev/{}", &ba.short)), + }; + let versions = + // using http is not a security concern and enabling tls makes mise significantly slower + match response { + Err(err) if http::error_code(&err) == Some(404) => return Ok(None), + res => res?, + }; + let versions = versions + .lines() + .map(|v| v.trim().to_string()) + .filter(|v| !v.is_empty()) + .collect::>(); + trace!( + "got {} {} versions from versions host", + versions.len(), + &ba.short + ); + match versions.is_empty() { + true => Ok(None), + false => Ok(Some(versions)), + } +} + +fn normalize_remote(remote: &str) -> eyre::Result { + let url = Url::parse(remote)?; + let host = url.host_str().unwrap(); + let path = url.path().trim_end_matches(".git"); + Ok(format!("{host}{path}")) +}