use std::collections::{HashMap, HashSet};
use eyre::{Report, bail};
use futures_util::stream::{self, StreamExt};
use super::api::{self, Formula};
use super::tag;
use crate::config::Settings;
use crate::result::Result;
use crate::system::packages::PackageRequest;
#[derive(Debug, Clone)]
pub(super) struct ResolvedFormula {
pub formula: Formula,
pub tap_raw_base: Option<String>,
pub on_request: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct FormulaKey {
name: String,
tap_name: Option<String>,
tap_url: Option<String>,
}
impl FormulaKey {
fn new(name: String, tap_name: Option<String>, tap_url: Option<String>) -> Self {
Self {
name,
tap_name,
tap_url,
}
}
}
fn accept_resolved_alias_failures(
failed: Vec<(FormulaKey, bool, Report)>,
canonical: &HashMap<FormulaKey, FormulaKey>,
on_request: &mut HashSet<FormulaKey>,
) -> Result<()> {
for (key, requested, err) in failed {
let Some(canonical_key) = canonical.get(&key).cloned() else {
return Err(err);
};
if requested {
on_request.insert(canonical_key);
}
}
Ok(())
}
pub(super) fn dep_tag(formula: &Formula, host_tag: &str) -> String {
if super::source::has_bottle(formula)
&& let Some((tag, _)) = formula.bottle_files().and_then(tag::select)
{
return tag;
}
host_tag.to_string()
}
fn install_deps<'a>(formula: &'a Formula, tag: &str) -> Vec<&'a String> {
let mut deps: Vec<&String> = formula.dependencies_for(tag).iter().collect();
if !super::source::has_bottle(formula) {
deps.extend(formula.build_dependencies_for(tag));
}
deps
}
pub(super) async fn resolve_closure_with_taps(
roots: &[PackageRequest],
provision_ruby: bool,
) -> Result<Vec<ResolvedFormula>> {
let roots = roots
.iter()
.map(|req| {
(
req.name.clone(),
api::tap_name(&req.name)
.or_else(|| req.tap_url.as_deref().and_then(api::tap_name_from_url)),
req.tap_url.clone(),
)
})
.collect::<Vec<_>>();
resolve_closure_pairs(&roots, provision_ruby).await
}
async fn resolve_closure_pairs(
roots: &[(String, Option<String>, Option<String>)],
provision_ruby: bool,
) -> Result<Vec<ResolvedFormula>> {
let host_tag = tag::host_tag();
let mut formulae: HashMap<FormulaKey, Formula> = HashMap::new();
let mut raw_bases: HashMap<FormulaKey, Option<String>> = HashMap::new();
let mut canonical: HashMap<FormulaKey, FormulaKey> = HashMap::new();
let mut on_request: HashSet<FormulaKey> = HashSet::new();
let mut queue: Vec<(FormulaKey, bool)> = roots
.iter()
.map(|(name, tap_name, tap_url)| {
(
FormulaKey::new(name.clone(), tap_name.clone(), tap_url.clone()),
true,
)
})
.collect();
while !queue.is_empty() {
let mut pending: Vec<(FormulaKey, bool)> = vec![];
let mut pending_positions: HashMap<FormulaKey, usize> = HashMap::new();
while let Some((key, requested)) = queue.pop() {
if let Some(canonical_key) = canonical.get(&key).cloned() {
if requested {
on_request.insert(canonical_key);
}
continue;
}
if let Some(index) = pending_positions.get(&key).copied() {
pending[index].1 |= requested;
} else {
pending_positions.insert(key.clone(), pending.len());
pending.push((key, requested));
}
}
let fetches: Vec<_> = pending
.into_iter()
.enumerate()
.map(|(index, (key, requested))| async move {
let result = fetch_formula_with_fallback(&key, requested, provision_ruby).await;
(index, key, requested, result)
})
.collect();
let mut fetched = stream::iter(fetches)
.buffer_unordered(crate::jobs::normalize(Settings::get().jobs).max(1))
.collect::<Vec<_>>()
.await;
fetched.sort_by_key(|(index, ..)| *index);
let mut failed = vec![];
for (_, key, requested, result) in fetched {
let (formula, effective_tap_name, effective_tap_url) = match result {
Ok(resolved) => resolved,
Err(err) => {
failed.push((key, requested, err));
continue;
}
};
if let Some(canonical_key) = canonical.get(&key).cloned() {
if requested {
on_request.insert(canonical_key);
}
continue;
}
let canonical_key = FormulaKey::new(
formula.name.clone(),
effective_tap_name.clone(),
effective_tap_url.clone(),
);
canonical.insert(key, canonical_key.clone());
canonical.insert(canonical_key.clone(), canonical_key.clone());
for alias in &formula.aliases {
canonical.insert(
FormulaKey::new(
alias.clone(),
effective_tap_name.clone(),
effective_tap_url.clone(),
),
canonical_key.clone(),
);
}
if !formulae.contains_key(&canonical_key) {
let tag = dep_tag(&formula, &host_tag);
for dep in install_deps(&formula, &tag) {
queue.push((
FormulaKey::new(
dep.clone(),
effective_tap_name.clone(),
effective_tap_url.clone(),
),
false,
));
}
raw_bases.insert(canonical_key.clone(), tap_raw_base(&canonical_key));
formulae.insert(canonical_key.clone(), formula);
}
if requested {
on_request.insert(canonical_key);
}
}
accept_resolved_alias_failures(failed, &canonical, &mut on_request)?;
}
let mut sorted: Vec<ResolvedFormula> = vec![];
let mut done: HashSet<FormulaKey> = HashSet::new();
let mut visiting: Vec<FormulaKey> = vec![];
struct VisitContext<'a> {
host_tag: &'a str,
formulae: &'a HashMap<FormulaKey, Formula>,
raw_bases: &'a HashMap<FormulaKey, Option<String>>,
canonical: &'a HashMap<FormulaKey, FormulaKey>,
done: &'a mut HashSet<FormulaKey>,
visiting: &'a mut Vec<FormulaKey>,
on_request: &'a HashSet<FormulaKey>,
sorted: &'a mut Vec<ResolvedFormula>,
}
fn visit(key: &FormulaKey, ctx: &mut VisitContext<'_>) -> Result<()> {
if ctx.done.contains(key) {
return Ok(());
}
if ctx.visiting.iter().any(|n| n == key) {
debug!("dependency cycle involving {}, breaking", key.name);
return Ok(());
}
let Some(formula) = ctx.formulae.get(key) else {
bail!("unresolved dependency: {}", key.name);
};
ctx.visiting.push(key.clone());
let tag = dep_tag(formula, ctx.host_tag);
for dep in install_deps(formula, &tag) {
let dep_key = FormulaKey::new(dep.clone(), key.tap_name.clone(), key.tap_url.clone());
let dep_key = ctx.canonical.get(&dep_key).cloned().unwrap_or(dep_key);
visit(&dep_key, ctx)?;
}
ctx.visiting.pop();
ctx.done.insert(key.clone());
ctx.sorted.push(ResolvedFormula {
formula: ctx.formulae[key].clone(),
tap_raw_base: ctx.raw_bases.get(key).cloned().flatten(),
on_request: ctx.on_request.contains(key),
});
Ok(())
}
let mut keys: Vec<FormulaKey> = formulae.keys().cloned().collect();
keys.sort_by(|a, b| {
a.tap_name
.cmp(&b.tap_name)
.then_with(|| a.tap_url.cmp(&b.tap_url))
.then_with(|| a.name.cmp(&b.name))
}); let mut visit_ctx = VisitContext {
host_tag: &host_tag,
formulae: &formulae,
raw_bases: &raw_bases,
canonical: &canonical,
done: &mut done,
visiting: &mut visiting,
on_request: &on_request,
sorted: &mut sorted,
};
for key in keys {
visit(&key, &mut visit_ctx)?;
}
Ok(sorted)
}
async fn fetch_formula(key: &FormulaKey, requested: bool, provision_ruby: bool) -> Result<Formula> {
if !requested && key.tap_name.is_some() && api::split_tap_name(&key.name).is_none() {
match api::formula(&key.name).await {
Ok(formula) => return Ok(formula),
Err(err) => {
debug!(
"brew: {} unavailable in core metadata ({err}); trying parent tap metadata",
key.name
);
}
}
}
api::formula_with_tap_name(
&key.name,
key.tap_name.as_deref(),
key.tap_url.as_deref(),
provision_ruby,
)
.await
}
async fn fetch_formula_with_fallback(
key: &FormulaKey,
requested: bool,
provision_ruby: bool,
) -> Result<(Formula, Option<String>, Option<String>)> {
match fetch_formula(key, requested, provision_ruby).await {
Ok(formula) => {
let effective_tap_name = match formula.tap.as_deref() {
Some("homebrew/core") => None,
Some(tap) => Some(tap.to_string()),
None => key.tap_name.clone(),
};
let effective_tap_url = effective_tap_name.as_ref().and(key.tap_url.clone());
Ok((formula, effective_tap_name, effective_tap_url))
}
Err(err) if key.tap_name.is_some() && api::split_tap_name(&key.name).is_none() => {
debug!(
"brew: {} unavailable in tap metadata ({err}); falling back to core metadata",
key.name
);
Ok((api::formula(&key.name).await?, None, None))
}
Err(err) => Err(err),
}
}
fn tap_raw_base(key: &FormulaKey) -> Option<String> {
let tap_name = key.tap_name.as_ref()?;
let formula_name = format!("{tap_name}/x");
let (owner, tap, _) = api::split_tap_name(&formula_name)?;
api::tap_raw_base(owner, tap, key.tap_url.as_deref())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn failed_alias_is_accepted_after_canonical_resolution() {
let canonical_key = FormulaKey::new("canonical".into(), None, None);
let alias_key = FormulaKey::new("alias".into(), None, None);
let canonical = HashMap::from([(alias_key.clone(), canonical_key.clone())]);
let mut on_request = HashSet::new();
accept_resolved_alias_failures(
vec![(alias_key, true, eyre::eyre!("redundant alias failed"))],
&canonical,
&mut on_request,
)
.unwrap();
assert_eq!(on_request, HashSet::from([canonical_key]));
}
#[test]
fn failed_unresolved_formula_is_preserved() {
let key = FormulaKey::new("missing".into(), None, None);
let err = accept_resolved_alias_failures(
vec![(key, false, eyre::eyre!("metadata failed"))],
&HashMap::new(),
&mut HashSet::new(),
)
.unwrap_err();
assert_eq!(err.to_string(), "metadata failed");
}
}