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
16 changes: 14 additions & 2 deletions src/backend/http.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down
62 changes: 47 additions & 15 deletions src/backend/static_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F, Fut, T>(
version: &str,
Expand Down Expand Up @@ -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<String> {
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);
Copy link

Choose a reason for hiding this comment

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

Bug: Key Parsing Error Causes Architecture Name Mismatch

The flat key parsing in list_available_platforms_with_key incorrectly converts all underscores to dashes when generating platform keys. This corrupts architecture names like x86_64 to x86-64, leading to mismatches with expected platform keys and incorrect suggestions.

Fix in Cursor Fix in Web

}
}
}

// 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)
Expand Down Expand Up @@ -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..];
Expand All @@ -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}"),
Expand Down Expand Up @@ -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()) {
Expand All @@ -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()) {
Expand Down
4 changes: 4 additions & 0 deletions src/toolset/tool_version_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ impl ToolVersionOptions {
self.get_nested_value_exists(key)
}

pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
self.opts.iter()
Comment on lines +62 to +63
Copy link

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

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

The iterator returns references to String rather than string slices. Consider returning impl Iterator<Item = (&str, &str)> for better ergonomics and to avoid unnecessary String allocations in consumer code.

Suggested change
pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
self.opts.iter()
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.opts.iter().map(|(k, v)| (k.as_str(), v.as_str()))

Copilot uses AI. Check for mistakes.
}

// 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
Expand Down
Loading