diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index bb0abb37a1..4ece0e67cf 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -65,49 +65,79 @@ impl<'a> AquaOptions<'a> { } fn var(&self, name: &str) -> Result> { - let opts = self.values.raw(); - if let Some(toml::Value::Table(vars)) = opts.opts.get("vars") - && let Some(value) = vars.get(name) - { - return toml_string_var(&format!("vars.{name}"), value).map(Some); - } - opts.opts + self.canonical_var_options() .get(name) - .map(|value| toml_string_var(name, value).map(Some)) + .map(|value| toml_string_var(&format!("vars.{name}"), value).map(Some)) .unwrap_or(Ok(None)) } fn lockfile_options(&self) -> BTreeMap { - let mut result = BTreeMap::new(); + self.canonical_var_options() + .into_iter() + .filter_map(|(key, value)| { + toml_value_to_string(value).map(|value| (format!("vars.{key}"), value)) + }) + .collect() + } + + fn canonical_var_options(&self) -> BTreeMap { + let mut ranked = BTreeMap::new(); for (key, value) in self.values.raw().iter() { if key == "symlink_bins" || EPHEMERAL_OPT_KEYS.contains(&key.as_str()) { continue; } + if key == "vars" { if let toml::Value::Table(table) = value { - Self::insert_vars_lockfile_options(&mut result, table); + Self::insert_nested_var_options(&mut ranked, table); } - } else if let Some(value) = toml_value_to_string(value) { - let key = if key.starts_with("vars.") { - key.clone() - } else { - format!("vars.{key}") - }; - result.entry(key).or_insert(value); + continue; } + + let (key, priority) = if let Some(key) = key.strip_prefix("vars.") { + (key.to_string(), AquaVarOptionPriority::Prefixed) + } else { + (key.clone(), AquaVarOptionPriority::Plain) + }; + Self::insert_var_option(&mut ranked, key, value, priority); + } + ranked + .into_iter() + .map(|(key, (_, value))| (key, value)) + .collect() + } + + fn insert_var_option<'b>( + result: &mut BTreeMap, + key: String, + value: &'b toml::Value, + priority: AquaVarOptionPriority, + ) { + if result + .get(&key) + .is_none_or(|(existing_priority, _)| priority > *existing_priority) + { + result.insert(key, (priority, value)); } - result } - fn insert_vars_lockfile_options(result: &mut BTreeMap, table: &toml::Table) { + fn insert_nested_var_options<'b>( + result: &mut BTreeMap, + table: &'b toml::Table, + ) { for (key, value) in table { - if let Some(value) = toml_value_to_string(value) { - result.insert(format!("vars.{key}"), value); - } + Self::insert_var_option(result, key.clone(), value, AquaVarOptionPriority::Nested); } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum AquaVarOptionPriority { + Prefixed, + Plain, + Nested, +} + #[derive(Debug, Clone, PartialEq, Eq)] struct AquaFileLink { src: PathBuf, @@ -2794,6 +2824,50 @@ mod tests { ); } + #[test] + fn test_apply_var_options_reads_prefixed_vars() { + let mut pkg = AquaPackage::default(); + pkg.asset = "tool-{{.Vars.channel}}-{{.Version}}.tar.gz".to_string(); + pkg.vars = vec![aqua_var("channel", true)]; + let mut opts = ToolVersionOptions::default(); + opts.opts.insert( + "vars.channel".to_string(), + toml::Value::String("stable".to_string()), + ); + + let opts = AquaOptions::new(&opts); + let pkg = AquaBackend::apply_var_options(pkg, &opts).unwrap(); + + assert_eq!( + pkg.asset("1.0.0", "linux", "amd64").unwrap(), + "tool-stable-1.0.0.tar.gz" + ); + } + + #[test] + fn test_apply_var_options_prefers_plain_vars_over_prefixed_vars() { + let mut pkg = AquaPackage::default(); + pkg.asset = "tool-{{.Vars.channel}}-{{.Version}}.tar.gz".to_string(); + pkg.vars = vec![aqua_var("channel", true)]; + let mut opts = ToolVersionOptions::default(); + opts.opts.insert( + "vars.channel".to_string(), + toml::Value::String("manifest".to_string()), + ); + opts.opts.insert( + "channel".to_string(), + toml::Value::String("stable".to_string()), + ); + + let opts = AquaOptions::new(&opts); + let pkg = AquaBackend::apply_var_options(pkg, &opts).unwrap(); + + assert_eq!( + pkg.asset("1.0.0", "linux", "amd64").unwrap(), + "tool-stable-1.0.0.tar.gz" + ); + } + #[test] fn test_apply_var_options_errors_for_array_vars() { let mut pkg = AquaPackage::default(); @@ -2887,10 +2961,6 @@ mod tests { #[test] fn test_lockfile_options_nested_aqua_vars_take_precedence() { let mut opts = ToolVersionOptions::default(); - opts.opts.insert( - "channel".to_string(), - toml::Value::String("stable".to_string()), - ); let mut vars = toml::Table::new(); vars.insert( "channel".to_string(), @@ -2898,12 +2968,33 @@ mod tests { ); opts.opts .insert("vars".to_string(), toml::Value::Table(vars)); + opts.opts.insert( + "channel".to_string(), + toml::Value::String("stable".to_string()), + ); let lock_opts = AquaOptions::new(&opts).lockfile_options(); assert_eq!(lock_opts.get("vars.channel"), Some(&"beta".to_string())); } + #[test] + fn test_lockfile_options_plain_aqua_vars_take_precedence_over_prefixed_vars() { + let mut opts = ToolVersionOptions::default(); + opts.opts.insert( + "channel".to_string(), + toml::Value::String("stable".to_string()), + ); + opts.opts.insert( + "vars.channel".to_string(), + toml::Value::String("manifest".to_string()), + ); + + let lock_opts = AquaOptions::new(&opts).lockfile_options(); + + assert_eq!(lock_opts.get("vars.channel"), Some(&"stable".to_string())); + } + #[test] fn test_aqua_install_time_options_include_flat_vars() { assert!(is_install_time_option_key("channel")); diff --git a/src/config/mod.rs b/src/config/mod.rs index ffddf0d56d..65ce2aefa6 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2880,6 +2880,79 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_resolve_tool_opts_preserves_aqua_symlink_bins_from_install_manifest() -> Result<()> + { + crate::toolset::install_state::init().await?; + + let source = crate::toolset::ToolSource::MiseToml(PathBuf::from("mise.toml")); + let config_ba = Arc::new(BackendArg::from("aqua:manifest-opts")); + let config_opts = crate::toolset::parse_tool_options("channel=stable"); + let mut trs = ToolRequestSet::new(); + trs.add_version( + crate::toolset::ToolRequest::new_opts(config_ba, "1.0.0", config_opts, source.clone())?, + &source, + ); + + let mut manifest_opts = BTreeMap::new(); + manifest_opts.insert( + "channel".to_string(), + toml::Value::String("manifest".to_string()), + ); + manifest_opts.insert( + "symlink_bins".to_string(), + toml::Value::String("true".to_string()), + ); + let ba = Arc::new(BackendArg::from( + crate::toolset::install_state::InstallStateTool { + short: "aqua:manifest-opts".to_string(), + full: Some("aqua:manifest-opts".to_string()), + versions: vec!["1.0.0".to_string()], + explicit_backend: true, + opts: manifest_opts, + installs_path: None, + }, + )); + + let config = Config { + tera_ctx: BASE_CONTEXT.clone(), + config_files: Default::default(), + env: OnceCell::new(), + env_with_sources: OnceCell::new(), + shorthands: get_shorthands(&Settings::get()), + hooks: OnceCell::new(), + tasks_cache: Arc::new(DashMap::new()), + tool_request_set: OnceCell::new(), + toolset: OnceCell::new(), + all_aliases: Default::default(), + aliases: Default::default(), + project_root: Default::default(), + repo_urls: Default::default(), + shell_aliases: Default::default(), + tera_files: Default::default(), + vars: Default::default(), + vars_loader: None, + vars_results: OnceCell::new(), + }; + config.tool_request_set.set(trs).ok(); + let config = Arc::new(config); + + let resolved = config.resolve_tool_opts_with_overrides(&ba).await?; + let opts = resolved.options(); + + assert_eq!(opts.get("channel"), Some("stable")); + assert_eq!( + resolved.source_for_key("channel"), + Some(crate::toolset::ToolOptionSource::Config) + ); + assert_eq!(opts.get("symlink_bins"), Some("true")); + assert_eq!( + resolved.source_for_key("symlink_bins"), + Some(crate::toolset::ToolOptionSource::InstallManifest) + ); + Ok(()) + } + #[tokio::test] async fn test_resolve_tool_opts_prefers_env_backend_override_over_alias_opts() -> Result<()> { unsafe {