use std::collections::{BTreeMap, HashMap};
use nova_diagnostics::{Diagnostic, Span};
use semver::{Version, VersionReq};
use crate::lock::{Lock, LockedPackage};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Candidate {
pub version: Version,
pub deps: Vec<(String, VersionReq)>,
pub checksum: String,
}
pub trait IndexView {
fn versions(&mut self, name: &str) -> Result<Option<Vec<Candidate>>, String>;
}
#[derive(Debug, Clone)]
pub struct Requirement {
pub name: String,
pub req: VersionReq,
pub by: String,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unlock {
Nothing,
All,
One(String),
}
#[derive(Debug)]
pub enum ResolveError {
Diagnostic(Diagnostic),
Index(String),
}
pub fn resolve(
requirements: &[Requirement],
lock: Option<&Lock>,
unlock: &Unlock,
index: &mut dyn IndexView,
) -> Result<Vec<LockedPackage>, ResolveError> {
let pins = |except: Option<&str>| -> HashMap<String, Version> {
lock.map(|lock| {
lock.packages
.iter()
.filter(|p| Some(p.name.as_str()) != except)
.map(|p| (p.name.clone(), p.version.clone()))
.collect()
})
.unwrap_or_default()
};
let mut search = Search {
index,
known: HashMap::new(),
pinned: HashMap::new(),
strict: false,
failure: None,
conflicted: Vec::new(),
};
match unlock {
Unlock::All => search.run(requirements, HashMap::new(), false),
Unlock::One(name) => search
.run(requirements, pins(Some(name.as_str())), true)
.map_err(|error| match error {
ResolveError::Diagnostic(d) if d.code == "M0015" => ResolveError::Diagnostic(
d.with_note("the other locked versions were kept; run `nova update` to let them move too"),
),
other => other,
}),
Unlock::Nothing => {
let mut pinned = pins(None);
loop {
match search.run(requirements, pinned.clone(), false) {
Err(ResolveError::Diagnostic(d)) if d.code == "M0015" && !pinned.is_empty() => {
let before = pinned.len();
for name in &search.conflicted {
pinned.remove(name);
}
if pinned.len() == before {
pinned.clear();
}
}
result => return result,
}
}
}
}
}
struct Failure {
name: String,
reqs: Vec<Requirement>,
}
struct State {
reqs: Vec<Requirement>,
order: Vec<String>,
chosen: BTreeMap<String, Candidate>,
}
struct Search<'a> {
index: &'a mut dyn IndexView,
known: HashMap<String, Vec<Candidate>>,
pinned: HashMap<String, Version>,
strict: bool,
failure: Option<Failure>,
conflicted: Vec<String>,
}
impl Search<'_> {
fn run(
&mut self,
requirements: &[Requirement],
pinned: HashMap<String, Version>,
strict: bool,
) -> Result<Vec<LockedPackage>, ResolveError> {
self.pinned = pinned;
self.strict = strict;
self.failure = None;
let mut order: Vec<String> = requirements.iter().map(|r| r.name.clone()).collect();
order.sort();
order.dedup();
let mut state = State {
reqs: requirements.to_vec(),
order,
chosen: BTreeMap::new(),
};
if self.solve(&mut state)? {
return Ok(state
.chosen
.into_iter()
.map(|(name, candidate)| {
let mut dependencies: Vec<String> =
candidate.deps.iter().map(|(n, _)| n.clone()).collect();
dependencies.sort();
dependencies.dedup();
LockedPackage {
name,
version: candidate.version,
checksum: candidate.checksum,
dependencies,
}
})
.collect());
}
let failure = self.failure.take().expect("a failed search records why");
self.conflicted = std::iter::once(failure.name.clone())
.chain(failure.reqs.iter().map(|r| r.by.clone()))
.collect();
Err(ResolveError::Diagnostic(conflict(&failure)))
}
fn solve(&mut self, state: &mut State) -> Result<bool, ResolveError> {
let Some(name) = state
.order
.iter()
.find(|n| !state.chosen.contains_key(*n))
.cloned()
else {
return Ok(true);
};
let reqs: Vec<Requirement> = state
.reqs
.iter()
.filter(|r| r.name == name)
.cloned()
.collect();
let fitting: Vec<Candidate> = self
.versions(&name, &reqs)?
.into_iter()
.filter(|c| reqs.iter().all(|r| r.req.matches(&c.version)))
.collect();
let kept = self
.pinned
.get(&name)
.map(|version| fitting.iter().position(|c| &c.version == version));
let tried = match kept {
Some(Some(i)) => vec![fitting[i].clone()],
Some(None) if self.strict => Vec::new(),
_ => fitting,
};
let via = reqs[0].span;
for candidate in tried {
let reqs_before = state.reqs.len();
let order_before = state.order.len();
let mut new_names = Vec::new();
for (dep, req) in &candidate.deps {
state.reqs.push(Requirement {
name: dep.clone(),
req: req.clone(),
by: name.clone(),
span: via,
});
if !state.order.contains(dep) && !new_names.contains(dep) {
new_names.push(dep.clone());
}
}
new_names.sort();
state.order.extend(new_names);
let clash = candidate.deps.iter().find(|(dep, req)| {
state
.chosen
.get(dep)
.is_some_and(|chosen| !req.matches(&chosen.version))
});
match clash {
Some((dep, _)) => {
let on_dep = state
.reqs
.iter()
.filter(|r| &r.name == dep)
.cloned()
.collect();
self.note(dep, on_dep);
}
None => {
state.chosen.insert(name.clone(), candidate.clone());
if self.solve(state)? {
return Ok(true);
}
state.chosen.remove(&name);
}
}
state.order.truncate(order_before);
state.reqs.truncate(reqs_before);
}
self.note(&name, reqs);
Ok(false)
}
fn versions(
&mut self,
name: &str,
reqs: &[Requirement],
) -> Result<Vec<Candidate>, ResolveError> {
if !self.known.contains_key(name) {
let Some(mut versions) = self.index.versions(name).map_err(ResolveError::Index)? else {
return Err(ResolveError::Diagnostic(
Diagnostic::error("M0014", format!("the index has no package `{name}`"))
.with_primary_label(reqs[0].span, "required here"),
));
};
versions.sort_by(|a, b| b.version.cmp(&a.version));
self.known.insert(name.to_string(), versions);
}
Ok(self.known[name].clone())
}
fn note(&mut self, name: &str, reqs: Vec<Requirement>) {
if self.failure.is_none() {
self.failure = Some(Failure {
name: name.to_string(),
reqs,
});
}
}
}
fn conflict(failure: &Failure) -> Diagnostic {
let wants: Vec<String> = failure
.reqs
.iter()
.map(|r| format!("`{}` from `{}`", r.req, r.by))
.collect();
Diagnostic::error(
"M0015",
format!(
"no version of `{}` meets every requirement: {}",
failure.name,
wants.join(", ")
),
)
.with_primary_label(failure.reqs[0].span, "required here")
}