diff --git a/build.rs b/build.rs index 52cb4343c14..1eb2e327145 100644 --- a/build.rs +++ b/build.rs @@ -9,7 +9,7 @@ use std::path::{Path, PathBuf}; use std::{env, fs}; use aqua_registry::encode_package_rkyv; -use aqua_registry::types::{AquaPackage, RegistryPackageRow, RegistryYaml}; +use aqua_registry::types::{AquaPackage, AquaPackageType, RegistryPackageRow, RegistryYaml}; use eyre::{Result, eyre}; use serde_yaml::Value; @@ -36,6 +36,17 @@ struct AquaPackageRegistry { id: String, content: Vec, aliases: Vec, + search_backend: Option, + search_overrides: Vec, +} + +#[derive(Debug)] +struct AquaSearchOverrideRegistry { + goos: Option, + goarch: Option, + envs: Vec, + libc: Option, + backend: Option, } /// Generate a raw string literal that safely contains the given content. @@ -428,6 +439,7 @@ fn codegen_aqua_standard_registry() -> Result<()> { let out_dir = env::var("OUT_DIR")?; let files_dest_path = Path::new(&out_dir).join("aqua_standard_registry_files.rs"); let aliases_dest_path = Path::new(&out_dir).join("aqua_standard_registry_aliases.rs"); + let search_dest_path = Path::new(&out_dir).join("aqua_standard_registry_search.rs"); let metadata_dest_path = Path::new(&out_dir).join("aqua_standard_registry_metadata.rs"); let packages_dir = Path::new(&out_dir).join("aqua_standard_registry_packages"); @@ -453,6 +465,7 @@ fn codegen_aqua_standard_registry() -> Result<()> { aqua_registry_files_code(®istries, &packages_dir)?, )?; fs::write(aliases_dest_path, aqua_registry_aliases_code(®istries)?)?; + fs::write(search_dest_path, aqua_registry_search_code(®istries))?; let metadata = serde_yaml::from_str::(&fs::read_to_string(metadata_file)?)?; let repository = yaml_string_field(&metadata, "repository").ok_or_else(|| { @@ -493,15 +506,77 @@ fn aqua_package_registries(rows: &[RegistryPackageRow]) -> Result>(); + if search_overrides + .iter() + .all(|package_override| package_override.backend == search_backend) + { + search_overrides.clear(); + } registries.push(AquaPackageRegistry { id, content, aliases: row.aliases.clone(), + search_backend, + search_overrides, }); } Ok(registries) } +fn aqua_search_backend(package: &AquaPackage, allow_go_repo_fallback: bool) -> Option { + match package.package_type() { + AquaPackageType::Cargo => package + .crate_name + .as_deref() + .or_else(|| { + package + .name + .as_deref() + .and_then(|name| name.strip_prefix("crates.io/")) + }) + .filter(|name| !is_aqua_template(name)) + .map(|name| format!("cargo:{name}")) + .or(Some(String::new())), + AquaPackageType::GoInstall => package + .path + .as_deref() + .filter(|path| !is_aqua_template(path)) + .map(|path| format!("go:{path}")) + .or_else(|| { + (allow_go_repo_fallback + && !package.repo_owner.is_empty() + && !package.repo_name.is_empty() + && !is_aqua_template(&package.repo_owner) + && !is_aqua_template(&package.repo_name)) + .then(|| format!("go:github.com/{}/{}", package.repo_owner, package.repo_name)) + }) + .or(Some(String::new())), + AquaPackageType::GoBuild => Some(String::new()), + AquaPackageType::GithubArchive + | AquaPackageType::GithubContent + | AquaPackageType::GithubRelease + | AquaPackageType::Http => None, + } +} + +fn is_aqua_template(value: &str) -> bool { + value.contains("{{") || value.contains("}}") +} + fn aqua_registry_files_code( registries: &[AquaPackageRegistry], packages_dir: &Path, @@ -584,6 +659,53 @@ fn aqua_registry_aliases_code(registries: &[AquaPackageRegistry]) -> Result String { + let entries = registries + .iter() + .filter_map(|registry| { + if registry.search_backend.is_none() && registry.search_overrides.is_empty() { + return None; + } + let overrides = registry + .search_overrides + .iter() + .map(|package_override| { + let envs = package_override + .envs + .iter() + .map(|env| format!("{env:?}")) + .collect::>() + .join(", "); + format!( + "AquaSearchBackendOverride {{ goos: {}, goarch: {}, envs: &[{envs}], libc: {}, backend: {} }}", + rust_option_str(package_override.goos.as_deref()), + rust_option_str(package_override.goarch.as_deref()), + rust_option_str(package_override.libc.as_deref()), + rust_option_str(package_override.backend.as_deref()), + ) + }) + .collect::>() + .join(", "); + Some(( + registry.id.clone(), + format!( + "AquaSearchBackends {{ default: {}, overrides: &[{overrides}] }}", + rust_option_str(registry.search_backend.as_deref()), + ), + )) + }) + .collect::>(); + let mut map = phf_codegen::Map::new(); + for (key, value) in &entries { + map.entry(key, value); + } + map.build().to_string() +} + +fn rust_option_str(value: Option<&str>) -> String { + value.map_or_else(|| "None".to_string(), |value| format!("Some({value:?})")) +} + fn aqua_registry_string_map_code(entries: &[(String, String)]) -> String { let mut map = phf_codegen::Map::new(); let mut values = Vec::new(); diff --git a/crates/aqua-registry/src/types.rs b/crates/aqua-registry/src/types.rs index eccc6c8f302..f0026b09897 100644 --- a/crates/aqua-registry/src/types.rs +++ b/crates/aqua-registry/src/types.rs @@ -131,6 +131,16 @@ struct AquaRuntime<'a> { libc: Option<&'a str>, } +/// A platform-specific package override with selectors mise can evaluate at runtime. +#[derive(Debug, Clone)] +pub struct AquaPackagePlatformOverride { + pub package: AquaPackage, + pub goos: Option, + pub goarch: Option, + pub envs: Vec, + pub libc: Option, +} + /// Variable definition for Aqua templates #[derive(Debug, Deserialize, Archive, RkyvDeserialize, RkyvSerialize, Clone, Default)] pub struct AquaVar { @@ -397,6 +407,59 @@ impl AquaPackage { self.with_version_runtime(versions, os, arch, AquaRuntime { libc }) } + /// Apply a catch-all version fallback when the root package is explicitly disabled. + /// + /// The boolean is false when the root package is explicitly disabled, + /// even if no catch-all override exists to replace it. Conditional roots + /// are preserved because determining whether they match requires a + /// concrete version. + pub fn with_unconditional_version_override(mut self) -> (AquaPackage, bool) { + if self.version_constraint.trim() != "false" { + return (self, true); + } + if let Some(version_override) = self + .version_overrides + .iter() + .find(|version_override| { + matches!(version_override.version_constraint.trim(), "" | "true") + }) + .cloned() + { + self = apply_override(self, &version_override); + } + (self, false) + } + + /// Return platform overrides after applying them to this package. + /// + /// Overrides containing runtime variants mise does not understand are + /// omitted because they can never match in the Aqua resolver either. + pub fn platform_overrides(&self) -> Vec { + self.overrides + .iter() + .filter_map(|package_override| { + let mut libc = None; + for variant in &package_override.variants { + if variant.key != "libc" { + return None; + } + let variant_libc = normalize_libc(Some(&variant.value))?.to_string(); + if libc.as_ref().is_some_and(|libc| libc != &variant_libc) { + return None; + } + libc = Some(variant_libc); + } + Some(AquaPackagePlatformOverride { + package: apply_override(self.clone(), &package_override.pkg), + goos: package_override.goos.clone(), + goarch: package_override.goarch.clone(), + envs: package_override.envs.clone(), + libc, + }) + }) + .collect() + } + fn with_version_runtime( mut self, versions: &[&str], @@ -2788,6 +2851,120 @@ packages: ); } + #[test] + fn test_unconditional_override_resolves_package_type_without_version() { + let yml = r#" +packages: + - type: github_release + version_constraint: "false" + version_overrides: + - version_constraint: Version == "v1.0.0" + type: cargo + crate: historical-tool + - version_constraint: "true" + type: go_build +"#; + let (pkg, root_package) = first_registry_package(yml).with_unconditional_version_override(); + + assert_eq!(pkg.package_type(), AquaPackageType::GoBuild); + assert_eq!(pkg.crate_name, None); + assert!(!root_package); + } + + #[test] + fn test_unconditional_root_does_not_apply_version_fallback() { + let yml = r#" +packages: + - type: github_release + version_overrides: + - version_constraint: "true" + type: cargo + crate: tool +"#; + let (pkg, root_package) = first_registry_package(yml).with_unconditional_version_override(); + + assert_eq!(pkg.package_type(), AquaPackageType::GithubRelease); + assert_eq!(pkg.crate_name, None); + assert!(root_package); + } + + #[test] + fn test_conditional_root_does_not_apply_version_fallback_without_version() { + let yml = r#" +packages: + - type: go_install + version_constraint: semver(">= 1.2.0") + version_overrides: + - version_constraint: "true" + type: github_release +"#; + let (pkg, root_package) = first_registry_package(yml).with_unconditional_version_override(); + + assert_eq!(pkg.package_type(), AquaPackageType::GoInstall); + assert!(root_package); + } + + #[test] + fn test_platform_overrides_expose_normalized_libc_selector() { + let yml = r#" +packages: + - type: github_release + overrides: + - goos: linux + variants: + - key: libc + value: glibc + type: cargo + crate: platform-tool +"#; + let pkg = first_registry_package(yml); + let package_override = pkg.platform_overrides().into_iter().next().unwrap(); + + assert_eq!(package_override.goos.as_deref(), Some("linux")); + assert_eq!(package_override.libc.as_deref(), Some("gnu")); + assert_eq!( + package_override.package.package_type(), + AquaPackageType::Cargo + ); + assert_eq!( + package_override.package.crate_name.as_deref(), + Some("platform-tool") + ); + } + + #[test] + fn test_unconditional_override_applies_matching_platform_type() { + let yml = r#" +packages: + - type: github_release + version_constraint: "false" + version_overrides: + - version_constraint: "true" + overrides: + - envs: + - darwin + - windows + type: cargo + crate: platform-tool +"#; + let (effective, _) = first_registry_package(yml).with_unconditional_version_override(); + + assert_eq!(effective.package_type(), AquaPackageType::GithubRelease); + let package_override = effective.platform_overrides().into_iter().next().unwrap(); + assert_eq!( + package_override.envs, + vec!["darwin".to_string(), "windows".to_string()] + ); + assert_eq!( + package_override.package.package_type(), + AquaPackageType::Cargo + ); + assert_eq!( + package_override.package.crate_name.as_deref(), + Some("platform-tool") + ); + } + #[test] fn test_override_envs_match_os() { let yml = r#" diff --git a/e2e/cli/test_did_you_mean b/e2e/cli/test_did_you_mean index 90c94334c6e..906398d58e1 100644 --- a/e2e/cli/test_did_you_mean +++ b/e2e/cli/test_did_you_mean @@ -12,5 +12,18 @@ assert_fail "mise use nod" "node" # Test: Typo of python should suggest python assert_fail "mise use pythn" "python" +# Test: Aqua Cargo and GoInstall packages should use runnable backend suggestions +assert_not_contains "mise use broot 2>&1 || true" "aqua:crates.io/broot" +assert_fail "mise use broot" "cargo:broot" +assert_not_contains "mise use benchstat 2>&1 || true" "aqua:golang.org/x/perf/cmd/benchstat" +assert_fail "mise use benchstat" "go:golang.org/x/perf/cmd/benchstat" +assert_fail "mise use mapotf" "go:github.com/Azure/mapotf" +assert_fail "mise use ifacemaker" "go:github.com/vburenin/ifacemaker" + +# Test: Aqua GoBuild packages should not appear in suggestions +assert_not_contains "mise use ycat 2>&1 || true" "aqua:goccy/go-yaml/ycat" +assert_not_contains "mise use ycat 2>&1 || true" "go:github.com/goccy/go-yaml" +assert_not_contains "mise use kics 2>&1 || true" "aqua:Checkmarx/kics" + # Test: Completely unrelated name should not show suggestions assert_fail "mise use somethingnonexistent" "not found in mise tool registry" diff --git a/e2e/cli/test_search b/e2e/cli/test_search index ad52686d93f..ecaf76bd238 100644 --- a/e2e/cli/test_search +++ b/e2e/cli/test_search @@ -15,3 +15,46 @@ assert "mise search --match-type equal jq" "jq Command-line JSON processor. htt assert_contains "mise search 7zip" "aqua:ip7z/7zip" assert_contains "mise search --match-type contains 7zip" "aqua:ip7z/7zip" assert_contains "mise search --match-type equal 7zip" "aqua:ip7z/7zip" +assert_contains "mise search aqua:ip7z/7zip" "aqua:ip7z/7zip" + +# Backend prefixes matching a shorthand do not dump translated aqua packages +assert_not_contains "mise search cargo" "cargo:broot" +assert_not_contains "mise search aqua" "aqua:" + +# Aqua Cargo and GoInstall packages use runnable backends in search results +assert_contains "mise search --match-type equal broot" "cargo:broot" +assert_not_contains "mise search --match-type equal broot" "aqua:crates.io/broot" +assert_contains "mise search cargo:broot" "cargo:broot" +assert_contains "mise search --match-type equal benchstat" "go:golang.org/x/perf/cmd/benchstat" +assert_not_contains "mise search --match-type equal benchstat" "aqua:golang.org/x/perf/cmd/benchstat" +assert_contains "mise search go:golang.org/x/perf/cmd/benchstat" "go:golang.org/x/perf/cmd/benchstat" + +# Pathless and conditional-root GoInstall packages use the repository fallback +assert_contains "mise search --match-type equal mapotf" "go:github.com/Azure/mapotf" +assert_contains "mise search --match-type equal ifacemaker" "go:github.com/vburenin/ifacemaker" + +# Runtime platform settings select precomputed platform overrides +assert_contains "MISE_OS=macos mise search --match-type equal rhit" "cargo:rhit" +assert_contains "MISE_OS=macos mise search --match-type equal dockfmt" "go:github.com/jessfraz/dockfmt" + +# Names are searchable by the aqua id even when the backend path tail differs +assert_contains "mise search --match-type equal mock" "go:go.uber.org/mock/mockgen" +assert_contains "mise search --match-type equal playwright-go" "go:github.com/playwright-community/playwright-go/cmd/playwright" + +# The aqua id form still finds entries whose current platform has a backend override +assert_contains "MISE_OS=macos mise search aqua:eza-community/eza" "cargo:eza" + +# Aqua GoBuild packages are excluded from search +assert_fail "mise search --match-type equal ycat" "not found in registry" +assert_fail "mise search --match-type equal kics" "not found in registry" + +# A standard shorthand still takes precedence over a translated Cargo fallback +assert_contains "mise search --match-type equal tokei" "tokei Count your code" +assert_not_contains "mise search --match-type equal tokei" "aqua:XAMPPRocky/tokei" +assert_not_contains "mise search --match-type equal tokei" "cargo:tokei" + +# Version-templated GoInstall paths cannot form runnable suggestions +assert_fail "mise search --match-type equal mosint" "not found in registry" + +# Disabled roots whose catch-all override is an error message are excluded +assert_fail "mise search --match-type equal gorename" "not found in registry" diff --git a/src/aqua/aqua_registry_wrapper.rs b/src/aqua/aqua_registry_wrapper.rs index 4d3eb3b5436..23d32d44ff8 100644 --- a/src/aqua/aqua_registry_wrapper.rs +++ b/src/aqua/aqua_registry_wrapper.rs @@ -476,36 +476,82 @@ fn github_repo_slug(registry_url: &str) -> Option<(String, String)> { } struct AquaSuggestionsCache { - name_to_ids: HashMap<&'static str, Vec<&'static str>>, + name_to_entries: HashMap<&'static str, Vec>, names: Vec<&'static str>, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct AquaSearchEntry { + pub id: &'static str, + backend_override: Option<&'static str>, +} + +impl AquaSearchEntry { + pub(crate) fn name(&self) -> &'static str { + self.id.rsplit_once('/').map_or(self.id, |(_, name)| name) + } + + pub(crate) fn backend(&self) -> String { + self.backend_override + .map(str::to_string) + .unwrap_or_else(|| format!("aqua:{}", self.id)) + } + + pub(crate) fn backend_matches(&self, query: &str) -> bool { + query.strip_prefix("aqua:") == Some(self.id) + || self.backend_override.is_some_and(|b| b == query) + } +} + +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct AquaSuggestion { + pub name: String, + pub backend: String, +} + +pub(crate) fn aqua_search_entries() -> impl Iterator { + super::standard_registry::search_entries().map(|(id, backend_override)| AquaSearchEntry { + id, + backend_override, + }) +} + static AQUA_SUGGESTIONS_CACHE: Lazy = Lazy::new(|| { - let ids = super::standard_registry::package_ids(); - let mut name_to_ids: HashMap<&'static str, Vec<&'static str>> = HashMap::new(); - for id in ids { - if let Some((_, name)) = id.rsplit_once('/') { - name_to_ids.entry(name).or_default().push(id); - } + let mut name_to_entries: HashMap<&'static str, Vec> = HashMap::new(); + for entry in aqua_search_entries() { + name_to_entries.entry(entry.name()).or_default().push(entry); + } + let names = name_to_entries.keys().copied().collect(); + AquaSuggestionsCache { + name_to_entries, + names, } - let names = name_to_ids.keys().copied().collect(); - AquaSuggestionsCache { name_to_ids, names } }); -/// Search aqua packages by tool name, returning "owner/name" IDs -/// where the name part is similar to the query. -pub(crate) fn aqua_suggest(query: &str) -> Vec { +/// Search Aqua packages by tool name, returning runnable mise backend suggestions. +pub(crate) fn aqua_suggest(query: &str) -> Vec { let cache = &*AQUA_SUGGESTIONS_CACHE; // Use a higher threshold (0.8) to avoid noisy suggestions let similar_names = xx::suggest::similar_n_with_threshold(query, &cache.names, 5, 0.8); - // Map back to full IDs let mut results = Vec::new(); for matched_name in &similar_names { - if let Some(full_ids) = cache.name_to_ids.get(matched_name.as_str()) { - for full_id in full_ids { - results.push(full_id.to_string()); + if let Some(entries) = cache.name_to_entries.get(matched_name.as_str()) { + for entry in entries { + let backend = entry.backend(); + // Distinct aqua packages can translate to the same backend + // (e.g. crates.io/eza and eza-community/eza both to cargo:eza) + if results + .iter() + .any(|suggestion: &AquaSuggestion| suggestion.backend == backend) + { + continue; + } + results.push(AquaSuggestion { + name: matched_name.clone(), + backend, + }); if results.len() >= 5 { return results; } diff --git a/src/aqua/standard_registry.rs b/src/aqua/standard_registry.rs index e52c37eb2a9..9dbafe57923 100644 --- a/src/aqua/standard_registry.rs +++ b/src/aqua/standard_registry.rs @@ -1,5 +1,7 @@ use aqua_registry::{AquaPackage, Result, decode_package_rkyv}; +use crate::platform::Platform; + /// Metadata for the baked aqua registry snapshot. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct AquaRegistryMetadata { @@ -23,9 +25,86 @@ static AQUA_STANDARD_REGISTRY_ALIASES: phf::Map<&'static str, &'static str> = in "/aqua_standard_registry_aliases.rs" )); -/// Returns all package IDs from the baked-in Aqua registry without allocating a collection. -pub(crate) fn package_ids() -> impl Iterator { - AQUA_STANDARD_REGISTRY_FILES.keys().copied() +#[derive(Debug)] +struct AquaSearchBackends { + default: Option<&'static str>, + overrides: &'static [AquaSearchBackendOverride], +} + +#[derive(Debug)] +struct AquaSearchBackendOverride { + goos: Option<&'static str>, + goarch: Option<&'static str>, + envs: &'static [&'static str], + libc: Option<&'static str>, + backend: Option<&'static str>, +} + +#[derive(Debug)] +struct AquaSearchPlatform { + os: String, + arch: String, + libc: Option, +} + +impl AquaSearchBackends { + fn backend(&self, platform: &AquaSearchPlatform) -> Option<&'static str> { + self.overrides + .iter() + .find(|package_override| package_override.matches(platform)) + .map_or(self.default, |package_override| package_override.backend) + } +} + +impl AquaSearchBackendOverride { + fn matches(&self, platform: &AquaSearchPlatform) -> bool { + self.goos.is_none_or(|goos| goos == platform.os.as_str()) + && self + .goarch + .is_none_or(|goarch| goarch == platform.arch.as_str()) + && (self.envs.is_empty() + || self.envs.iter().any(|env| { + *env == "all" + || *env == platform.os.as_str() + || *env == platform.arch.as_str() + || env.split_once('/') + == Some((platform.os.as_str(), platform.arch.as_str())) + })) + && self + .libc + .is_none_or(|libc| Some(libc) == platform.libc.as_deref()) + } +} + +/// Baked exceptions to the default `aqua:` search backend. +/// An empty backend marks a package that cannot be represented by a runnable mise backend. +static AQUA_STANDARD_REGISTRY_SEARCH: phf::Map<&'static str, AquaSearchBackends> = include!( + concat!(env!("OUT_DIR"), "/aqua_standard_registry_search.rs") +); + +/// Returns searchable Aqua package IDs and any precomputed backend override. +/// Packages without a runnable mise backend on the current platform are omitted. +pub(crate) fn search_entries() -> impl Iterator)> { + let current_platform = Platform::current(); + let os = match current_platform.os.as_str() { + "macos" => "darwin", + other => other, + } + .to_string(); + let arch = match current_platform.arch.as_str() { + "x64" => "amd64", + other => other, + } + .to_string(); + let libc = (os == "linux").then(|| current_platform.libc().unwrap_or("gnu").to_string()); + let platform = AquaSearchPlatform { os, arch, libc }; + + AQUA_STANDARD_REGISTRY_FILES.keys().filter_map(move |id| { + let backend = AQUA_STANDARD_REGISTRY_SEARCH + .get(id) + .and_then(|backends| backends.backend(&platform)); + (backend != Some("")).then_some((*id, backend)) + }) } pub(crate) fn package(package_id: &str) -> Option> { @@ -65,6 +144,82 @@ mod tests { ); } + #[test] + fn test_baked_registry_search_entries() { + let entries = search_entries().collect::>(); + + assert_eq!(entries.get("crates.io/broot"), Some(&Some("cargo:broot"))); + assert_eq!( + entries.get("golang.org/x/perf/cmd/benchstat"), + Some(&Some("go:golang.org/x/perf/cmd/benchstat")) + ); + assert_eq!(entries.get("goccy/go-yaml/ycat"), None); + assert_eq!( + entries.get("Azure/mapotf"), + Some(&Some("go:github.com/Azure/mapotf")) + ); + assert_eq!( + entries.get("vburenin/ifacemaker"), + Some(&Some("go:github.com/vburenin/ifacemaker")) + ); + assert_eq!(entries.get("golang/tools/gorename"), None); + assert_eq!(entries.get("cli/cli"), Some(&None)); + } + + #[test] + fn test_baked_registry_search_platform_overrides() { + let darwin = AquaSearchPlatform { + os: "darwin".to_string(), + arch: "arm64".to_string(), + libc: None, + }; + let linux = AquaSearchPlatform { + os: "linux".to_string(), + arch: "amd64".to_string(), + libc: Some("gnu".to_string()), + }; + + let rhit = AQUA_STANDARD_REGISTRY_SEARCH.get("Canop/rhit").unwrap(); + assert_eq!(rhit.backend(&darwin), Some("cargo:rhit")); + assert_eq!(rhit.backend(&linux), None); + + let dockfmt = AQUA_STANDARD_REGISTRY_SEARCH + .get("jessfraz/dockfmt") + .unwrap(); + assert_eq!( + dockfmt.backend(&darwin), + Some("go:github.com/jessfraz/dockfmt") + ); + assert_eq!(dockfmt.backend(&linux), None); + } + + #[test] + fn test_search_backend_libc_override_selection() { + let backends = AquaSearchBackends { + default: None, + overrides: &[AquaSearchBackendOverride { + goos: Some("linux"), + goarch: None, + envs: &[], + libc: Some("musl"), + backend: Some("cargo:example"), + }], + }; + let gnu = AquaSearchPlatform { + os: "linux".to_string(), + arch: "amd64".to_string(), + libc: Some("gnu".to_string()), + }; + let musl = AquaSearchPlatform { + os: "linux".to_string(), + arch: "amd64".to_string(), + libc: Some("musl".to_string()), + }; + + assert_eq!(backends.backend(&gnu), None); + assert_eq!(backends.backend(&musl), Some("cargo:example")); + } + #[test] fn test_baked_registry_metadata() { assert_eq!( diff --git a/src/cli/args/backend_arg.rs b/src/cli/args/backend_arg.rs index 89775113c20..9cb1936c3e6 100644 --- a/src/cli/args/backend_arg.rs +++ b/src/cli/args/backend_arg.rs @@ -360,13 +360,9 @@ impl BackendArg { .collect(); let mise_names: HashSet = suggestions.iter().cloned().collect(); - for aqua_id in crate::aqua::aqua_registry_wrapper::aqua_suggest(&self.short) { - // Skip aqua suggestions whose tool name matches an existing mise suggestion - let name = aqua_id - .rsplit_once('/') - .map_or(aqua_id.as_str(), |(_, n)| n); - if !mise_names.contains(name) { - suggestions.push(format!("aqua:{aqua_id}")); + for aqua in crate::aqua::aqua_registry_wrapper::aqua_suggest(&self.short) { + if !mise_names.contains(&aqua.name) { + suggestions.push(aqua.backend); } } diff --git a/src/cli/search.rs b/src/cli/search.rs index 6cdb5023602..cf9405c1d9b 100644 --- a/src/cli/search.rs +++ b/src/cli/search.rs @@ -160,31 +160,41 @@ impl Search { return vec![]; } - crate::aqua::standard_registry::package_ids() - .filter_map(|id| { - let tool_name = id.rsplit_once('/').map_or(id, |(_, name)| name); - let score = match self.match_type { - MatchType::Equal => { - if tool_name == name || id == name || format!("aqua:{id}") == name { - Some(0) - } else { - None + crate::aqua::aqua_registry_wrapper::aqua_search_entries() + .filter_map(|entry| { + let tool_name = entry.name(); + let score = if entry.backend_matches(name) { + Some(0) + } else { + match self.match_type { + MatchType::Equal => { + if tool_name == name || entry.id == name { + Some(0) + } else { + None + } } - } - MatchType::Contains => { - if tool_name.contains(name) || id.contains(name) { - Some(0) - } else { - None + MatchType::Contains => { + if tool_name.contains(name) || entry.id.contains(name) { + Some(0) + } else { + None + } + } + MatchType::Fuzzy => { + fuzzy_matcher.score_pattern(&tool_name.to_lowercase(), fuzzy_pattern) } - } - MatchType::Fuzzy => { - fuzzy_matcher.score_pattern(&tool_name.to_lowercase(), fuzzy_pattern) } }?; - Some((score, format!("aqua:{id}"), get_aqua_description(id))) + let search_backend = entry.backend(); + let description = get_aqua_description(entry.id, &search_backend); + + Some((score, search_backend, description)) }) + // Distinct aqua packages can translate to the same backend + // (e.g. crates.io/eza and eza-community/eza both to cargo:eza) + .unique_by(|(_score, search_backend, _description)| search_backend.clone()) .collect() } @@ -251,20 +261,25 @@ fn get_backends(backends: Vec<&'static str>) -> Vec { let prefix = backend.split(':').next().unwrap_or(""); let slug = backend.split(':').next_back().unwrap_or(""); let slug = regex!(r"^(.*?)\[.*\]$").replace_all(slug, "$1"); - match prefix { - "core" => format!("https://mise.jdx.dev/lang/{slug}.html"), - "cargo" => format!("https://crates.io/crates/{slug}"), - "go" => format!("https://pkg.go.dev/{slug}"), - "pipx" => format!("https://pypi.org/project/{slug}"), - "npm" => format!("https://www.npmjs.com/package/{slug}"), - _ => format!("https://github.com/{slug}"), - } + backend_homepage_url(prefix, &slug) + .unwrap_or_else(|| format!("https://github.com/{slug}")) }) .collect() } -fn get_aqua_description(id: &str) -> String { - let fallback = format!("aqua:{id}"); +fn backend_homepage_url(prefix: &str, slug: &str) -> Option { + match prefix { + "core" => Some(format!("https://mise.jdx.dev/lang/{slug}.html")), + "cargo" => Some(format!("https://crates.io/crates/{slug}")), + "go" => Some(format!("https://pkg.go.dev/{slug}")), + "pipx" => Some(format!("https://pypi.org/project/{slug}")), + "npm" => Some(format!("https://www.npmjs.com/package/{slug}")), + _ => None, + } +} + +fn get_aqua_description(id: &str, search_backend: &str) -> String { + let fallback = search_backend.to_string(); let Ok(pkg) = crate::aqua::standard_registry::package(id).unwrap_or_else(|| Ok(Default::default())) else { @@ -274,7 +289,8 @@ fn get_aqua_description(id: &str) -> String { let backend = if !pkg.repo_owner.is_empty() && !pkg.repo_name.is_empty() { format!("https://github.com/{}/{}", pkg.repo_owner, pkg.repo_name) } else { - fallback + let (backend_type, tool) = search_backend.split_once(':').unwrap_or_default(); + backend_homepage_url(backend_type, tool).unwrap_or(fallback) }; match pkg.description.as_deref().filter(|d| !d.is_empty()) {