use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use indexmap::IndexMap;
use semver::{Version, VersionReq};
use super::compat::CompatibilityResult;
use crate::config::{FilterMode, Manifest, SourceSpec};
use crate::error::ResolutionError;
use crate::lock::ItemKind;
use crate::source::ResolvedRef;
use crate::types::{ItemName, SourceId, SourceName};
#[derive(Debug, Clone)]
pub struct ResolvedGraph {
pub nodes: IndexMap<SourceName, ResolvedNode>,
pub order: Vec<SourceName>,
pub filters: HashMap<SourceName, Vec<FilterMode>>,
}
#[derive(Debug, Clone)]
pub struct ResolvedNode {
pub source_name: SourceName,
pub source_id: SourceId,
pub rooted_ref: RootedSourceRef,
pub resolved_ref: ResolvedRef,
pub latest_version: Option<Version>,
pub manifest: Option<Manifest>,
pub deps: Vec<SourceName>,
}
#[derive(Debug, Clone)]
pub struct RootedSourceRef {
pub checkout_root: PathBuf,
pub package_root: PathBuf,
}
#[derive(Debug, Clone)]
pub enum VersionConstraint {
Semver(VersionReq),
Latest,
RefPin(String),
}
impl std::fmt::Display for VersionConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VersionConstraint::Semver(req) => write!(f, "{req}"),
VersionConstraint::Latest => write!(f, "latest"),
VersionConstraint::RefPin(reference) => write!(f, "ref:{reference}"),
}
}
}
#[derive(Debug, Clone)]
pub struct PendingItem {
pub package: SourceName,
pub item: ItemName,
pub kind: ItemKind,
pub constraint: VersionConstraint,
pub required_by: String,
pub is_local: bool,
pub spec: SourceSpec,
}
#[derive(Debug)]
pub enum VersionCheckResult {
NotSeen,
SameVersion,
PotentiallyConflicting {
existing: VersionConstraint,
requested: VersionConstraint,
},
DifferentVersion {
existing: VersionConstraint,
requested: VersionConstraint,
},
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
struct VisitedItem {
package: SourceName,
item: ItemName,
}
#[derive(Debug, Clone)]
pub struct ResolvedVersion {
pub constraint: VersionConstraint,
pub resolved_ref: ResolvedRef,
}
pub struct VisitedSet {
index: HashMap<(SourceName, ItemName), ResolvedVersion>,
}
impl Default for VisitedSet {
fn default() -> Self {
Self::new()
}
}
impl VisitedSet {
pub fn new() -> Self {
Self {
index: HashMap::new(),
}
}
fn index_key(package: &SourceName, item: &ItemName) -> (SourceName, ItemName) {
let key = VisitedItem {
package: package.clone(),
item: item.clone(),
};
(key.package, key.item)
}
pub fn check_version(
&self,
package: &SourceName,
item: &ItemName,
constraint: &VersionConstraint,
) -> VersionCheckResult {
match self.index.get(&Self::index_key(package, item)) {
None => VersionCheckResult::NotSeen,
Some(existing) => match existing
.constraint
.compatible_with_resolved(constraint, existing.resolved_ref.version.as_ref())
{
CompatibilityResult::Compatible => VersionCheckResult::SameVersion,
CompatibilityResult::PotentiallyConflicting => {
VersionCheckResult::PotentiallyConflicting {
existing: existing.constraint.clone(),
requested: constraint.clone(),
}
}
CompatibilityResult::Conflicting => VersionCheckResult::DifferentVersion {
existing: existing.constraint.clone(),
requested: constraint.clone(),
},
},
}
}
pub fn insert(
&mut self,
package: SourceName,
item: ItemName,
constraint: VersionConstraint,
resolved_ref: ResolvedRef,
) {
self.index.insert(
Self::index_key(&package, &item),
ResolvedVersion {
constraint,
resolved_ref,
},
);
}
pub fn iter(&self) -> impl Iterator<Item = (&(SourceName, ItemName), &ResolvedVersion)> {
self.index.iter()
}
}
pub struct PackageVersions {
versions: HashMap<SourceName, (ResolvedRef, VersionConstraint, String)>,
}
impl Default for PackageVersions {
fn default() -> Self {
Self::new()
}
}
impl PackageVersions {
pub fn new() -> Self {
Self {
versions: HashMap::new(),
}
}
pub fn check_or_insert(
&mut self,
package: &SourceName,
resolved: &ResolvedRef,
requested: &VersionConstraint,
required_by: &str,
is_local: bool,
) -> Result<(), ResolutionError> {
if is_local {
return Ok(());
}
match self.versions.entry(package.clone()) {
Entry::Vacant(entry) => {
entry.insert((resolved.clone(), requested.clone(), required_by.to_string()));
Ok(())
}
Entry::Occupied(entry) => {
let (existing_ref, existing_constraint, existing_by) = entry.get();
match existing_constraint.compatible_with_resolved(
requested,
existing_ref.version.as_ref().or(resolved.version.as_ref()),
) {
CompatibilityResult::Compatible
| CompatibilityResult::PotentiallyConflicting => {
if resolved_ref_matches(existing_ref, resolved) {
Ok(())
} else {
Err(ResolutionError::PackageVersionConflict {
package: package.to_string(),
existing: format!("{existing_ref:?} (required by {existing_by})"),
requested: format!("{resolved:?} (required by {required_by})"),
chain: required_by.to_string(),
})
}
}
CompatibilityResult::Conflicting => {
Err(ResolutionError::PackageVersionConflict {
package: package.to_string(),
existing: format!("{existing_constraint} (required by {existing_by})"),
requested: format!("{requested} (required by {required_by})"),
chain: required_by.to_string(),
})
}
}
}
}
}
}
fn resolved_ref_matches(existing: &ResolvedRef, incoming: &ResolvedRef) -> bool {
existing.source_name == incoming.source_name
&& existing.version == incoming.version
&& existing.version_tag == incoming.version_tag
&& existing.commit == incoming.commit
&& crate::target::paths_equivalent(
&existing.tree_path.to_string_lossy(),
&incoming.tree_path.to_string_lossy(),
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolveMode {
Sync,
Frozen,
Upgrade {
targets: HashSet<SourceName>,
bump_direct_constraints: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolveOptions {
pub mode: ResolveMode,
}
impl Default for ResolveOptions {
fn default() -> Self {
Self {
mode: ResolveMode::Sync,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum VersionSelectionPolicy {
PreferLockThenLatest,
LatestOnly,
LockOnly,
}
impl ResolveOptions {
pub fn sync() -> Self {
Self {
mode: ResolveMode::Sync,
}
}
pub fn frozen() -> Self {
Self {
mode: ResolveMode::Frozen,
}
}
pub fn upgrade(targets: HashSet<SourceName>, bump_direct_constraints: bool) -> Self {
Self {
mode: ResolveMode::Upgrade {
targets,
bump_direct_constraints,
},
}
}
pub(crate) fn direct_constraint_for(
&self,
source_name: &SourceName,
declared: VersionConstraint,
) -> VersionConstraint {
if matches!(
&self.mode,
ResolveMode::Upgrade {
bump_direct_constraints: true,
..
}
) && self.is_upgrade_target(source_name)
{
VersionConstraint::Latest
} else {
declared
}
}
pub(crate) fn is_upgrade_target(&self, source_name: &SourceName) -> bool {
match &self.mode {
ResolveMode::Upgrade { targets, .. } => {
targets.is_empty() || targets.contains(source_name)
}
ResolveMode::Sync | ResolveMode::Frozen => false,
}
}
pub(crate) fn version_selection_policy(
&self,
source_name: &SourceName,
) -> VersionSelectionPolicy {
match &self.mode {
ResolveMode::Frozen => VersionSelectionPolicy::LockOnly,
ResolveMode::Upgrade { .. } if self.is_upgrade_target(source_name) => {
VersionSelectionPolicy::LatestOnly
}
ResolveMode::Sync | ResolveMode::Upgrade { .. } => {
VersionSelectionPolicy::PreferLockThenLatest
}
}
}
}