diff --git a/src/backend/http.rs b/src/backend/http.rs index 36f693846e..7c6ac790f0 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -1,7 +1,8 @@ use crate::backend::Backend; use crate::backend::backend_type::BackendType; use crate::backend::static_helpers::{ - clean_binary_name, get_filename_from_url, lookup_platform_key, template_string, verify_artifact, + clean_binary_name, get_filename_from_url, list_available_platforms_with_key, + lookup_platform_key, template_string, verify_artifact, }; use crate::cli::args::BackendArg; use crate::config::Config; @@ -306,7 +307,18 @@ impl Backend for HttpBackend { // Use the new helper to get platform-specific URL first, then fall back to general URL let url_template = lookup_platform_key(&opts, "url") .or_else(|| opts.get("url").cloned()) - .ok_or_else(|| eyre::eyre!("Http backend requires 'url' option"))?; + .ok_or_else(|| { + let platform_key = self.get_platform_key(); + let available = list_available_platforms_with_key(&opts, "url"); + if !available.is_empty() { + let list = available.join(", "); + eyre::eyre!( + "No URL configured for platform {platform_key}. Available platforms: {list}. Provide 'url' or add 'platforms.{platform_key}.url'" + ) + } else { + eyre::eyre!("Http backend requires 'url' option") + } + })?; // Template the URL with actual values let url = template_string(&url_template, &tv); diff --git a/src/backend/static_helpers.rs b/src/backend/static_helpers.rs index 82a8e6193f..d9f992a136 100644 --- a/src/backend/static_helpers.rs +++ b/src/backend/static_helpers.rs @@ -5,8 +5,19 @@ use crate::toolset::ToolVersion; use crate::toolset::ToolVersionOptions; use crate::ui::progress_report::SingleReport; use eyre::{Result, bail}; +use indexmap::IndexSet; use std::path::Path; +// Shared OS/arch patterns used across helpers +const OS_PATTERNS: &[&str] = &[ + "linux", "darwin", "macos", "windows", "win", "freebsd", "openbsd", "netbsd", "android", +]; +// Longer arch patterns first to avoid partial matches +const ARCH_PATTERNS: &[&str] = &[ + "x86_64", "aarch64", "ppc64le", "ppc64", "armv7", "armv6", "arm64", "amd64", "mipsel", + "riscv64", "s390x", "i686", "i386", "x64", "mips", "arm", "x86", +]; + /// Helper to try both prefixed and non-prefixed tags for a resolver function pub async fn try_with_v_prefix( version: &str, @@ -112,6 +123,38 @@ pub fn lookup_platform_key(opts: &ToolVersionOptions, key_type: &str) -> Option< None } +/// Lists platform keys (e.g. "macos-x64") for which a given key_type exists (e.g. "url"). +pub fn list_available_platforms_with_key(opts: &ToolVersionOptions, key_type: &str) -> Vec { + let mut set = IndexSet::new(); + + // Gather from flat keys + for (k, _) in opts.iter() { + if let Some(rest) = k + .strip_prefix("platforms_") + .or_else(|| k.strip_prefix("platform_")) + { + if let Some(platform_part) = rest.strip_suffix(&format!("_{}", key_type)) { + let platform_key = platform_part.replace('_', "-"); + set.insert(platform_key); + } + } + } + + // Probe nested keys using shared patterns + for os in OS_PATTERNS { + for arch in ARCH_PATTERNS { + for prefix in ["platforms", "platform"] { + let nested_key = format!("{prefix}.{os}-{arch}.{key_type}"); + if opts.contains_key(&nested_key) { + set.insert(format!("{os}-{arch}")); + } + } + } + } + + set.into_iter().collect() +} + pub fn template_string(template: &str, tv: &ToolVersion) -> String { let version = &tv.version; template.replace("{version}", version) @@ -245,17 +288,6 @@ pub fn verify_checksum_str( /// - "app-2.0.0-linux-x64" -> "app" (with tool_name="app") /// - "script-darwin-arm64.sh" -> "script.sh" (preserves .sh extension) pub fn clean_binary_name(name: &str, tool_name: Option<&str>) -> String { - // Common OS patterns to remove - let os_patterns = [ - "linux", "darwin", "macos", "windows", "win", "freebsd", "openbsd", "netbsd", "android", - ]; - - // Common architecture patterns to remove (longer patterns first to avoid partial matches) - let arch_patterns = [ - "x86_64", "aarch64", "ppc64le", "ppc64", "armv7", "armv6", "arm64", "amd64", "mipsel", - "riscv64", "s390x", "i686", "i386", "x64", "mips", "arm", "x86", - ]; - // Extract extension if present (to preserve it) let (name_without_ext, extension) = if let Some(pos) = name.rfind('.') { let potential_ext = &name[pos + 1..]; @@ -277,8 +309,8 @@ pub fn clean_binary_name(name: &str, tool_name: Option<&str>) -> String { let mut cleaned = name_without_ext.to_string(); // First try combined OS-arch patterns - for os in &os_patterns { - for arch in &arch_patterns { + for os in OS_PATTERNS { + for arch in ARCH_PATTERNS { // Try different separator combinations let patterns = [ format!("-{os}-{arch}"), @@ -306,7 +338,7 @@ pub fn clean_binary_name(name: &str, tool_name: Option<&str>) -> String { } // Try just OS suffix (sometimes arch is omitted) - for os in &os_patterns { + for os in OS_PATTERNS { let patterns = [format!("-{os}"), format!("_{os}")]; for pattern in &patterns { if let Some(pos) = cleaned.rfind(pattern.as_str()) { @@ -331,7 +363,7 @@ pub fn clean_binary_name(name: &str, tool_name: Option<&str>) -> String { } // Try just arch suffix (sometimes OS is omitted) - for arch in &arch_patterns { + for arch in ARCH_PATTERNS { let patterns = [format!("-{arch}"), format!("_{arch}")]; for pattern in &patterns { if let Some(pos) = cleaned.rfind(pattern.as_str()) { diff --git a/src/toolset/tool_version_options.rs b/src/toolset/tool_version_options.rs index d38827f20e..e50d54698e 100644 --- a/src/toolset/tool_version_options.rs +++ b/src/toolset/tool_version_options.rs @@ -59,6 +59,10 @@ impl ToolVersionOptions { self.get_nested_value_exists(key) } + pub fn iter(&self) -> impl Iterator { + self.opts.iter() + } + // Check if a nested value exists without returning a reference fn get_nested_value_exists(&self, key: &str) -> bool { // Split the key by dots to navigate nested structure