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
124 changes: 123 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -36,6 +36,17 @@ struct AquaPackageRegistry {
id: String,
content: Vec<u8>,
aliases: Vec<String>,
search_backend: Option<String>,
search_overrides: Vec<AquaSearchOverrideRegistry>,
}

#[derive(Debug)]
struct AquaSearchOverrideRegistry {
goos: Option<String>,
goarch: Option<String>,
envs: Vec<String>,
libc: Option<String>,
backend: Option<String>,
}

/// Generate a raw string literal that safely contains the given content.
Expand Down Expand Up @@ -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");

Expand All @@ -453,6 +465,7 @@ fn codegen_aqua_standard_registry() -> Result<()> {
aqua_registry_files_code(&registries, &packages_dir)?,
)?;
fs::write(aliases_dest_path, aqua_registry_aliases_code(&registries)?)?;
fs::write(search_dest_path, aqua_registry_search_code(&registries))?;

let metadata = serde_yaml::from_str::<Value>(&fs::read_to_string(metadata_file)?)?;
let repository = yaml_string_field(&metadata, "repository").ok_or_else(|| {
Expand Down Expand Up @@ -493,15 +506,77 @@ fn aqua_package_registries(rows: &[RegistryPackageRow]) -> Result<Vec<AquaPackag
));
}
let content = encode_package_rkyv(package)?;
let (effective_package, allow_go_repo_fallback) =
package.clone().with_unconditional_version_override();
let search_backend = aqua_search_backend(&effective_package, allow_go_repo_fallback);
let mut search_overrides = effective_package
.platform_overrides()
.into_iter()
.map(|package_override| AquaSearchOverrideRegistry {
goos: package_override.goos,
goarch: package_override.goarch,
envs: package_override.envs,
libc: package_override.libc,
backend: aqua_search_backend(&package_override.package, allow_go_repo_fallback),
})
.collect::<Vec<_>>();
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<String> {
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,
Expand Down Expand Up @@ -584,6 +659,53 @@ fn aqua_registry_aliases_code(registries: &[AquaPackageRegistry]) -> Result<Stri
Ok(aqua_registry_string_map_code(&entries))
}

fn aqua_registry_search_code(registries: &[AquaPackageRegistry]) -> 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::<Vec<_>>()
.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::<Vec<_>>()
.join(", ");
Some((
registry.id.clone(),
format!(
"AquaSearchBackends {{ default: {}, overrides: &[{overrides}] }}",
rust_option_str(registry.search_backend.as_deref()),
),
))
})
.collect::<Vec<_>>();
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();
Expand Down
177 changes: 177 additions & 0 deletions crates/aqua-registry/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub goarch: Option<String>,
pub envs: Vec<String>,
pub libc: Option<String>,
}

/// Variable definition for Aqua templates
#[derive(Debug, Deserialize, Archive, RkyvDeserialize, RkyvSerialize, Clone, Default)]
pub struct AquaVar {
Expand Down Expand Up @@ -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<AquaPackagePlatformOverride> {
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],
Expand Down Expand Up @@ -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#"
Expand Down
13 changes: 13 additions & 0 deletions e2e/cli/test_did_you_mean
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading
Loading