use std::fmt::Write;
use std::sync::Arc;
use rustc_hash::FxHashMap;
use tracing::{Level, trace};
use uv_distribution::Metadata;
use uv_distribution_types::{Dist, DistributionId, Identifier, IndexUrl, ResolvedDist};
use uv_git::GitResolver;
use uv_normalize::PackageName;
use uv_pep440::{MIN_VERSION, Version};
use uv_pep508::MarkerTree;
use uv_pypi_types::VerbatimParsedUrl;
use uv_resolver_types::PackageNodeKind;
use crate::pins::FilePins;
use crate::redirect::url_to_precise;
use crate::universal_marker::ConflictMarker;
use crate::{InMemoryIndex, MetadataResponse, ResolveError, ResolverEnvironment, UniversalMarker};
#[derive(Debug)]
pub(crate) struct Resolution<'index> {
pub(crate) nodes: FxHashMap<ResolutionPackage, Version>,
pub(crate) edges: Vec<ResolutionDependencyEdge>,
pub(crate) pins: FilePins<'index>,
pub(crate) env: ResolverEnvironment,
}
#[derive(Debug)]
pub(crate) struct ResolvedFork {
pub(crate) nodes: Vec<(ResolutionPackage, SelectedDistribution)>,
pub(crate) edges: Vec<ResolutionDependencyEdge>,
pub(crate) env: ResolverEnvironment,
}
#[derive(Debug)]
pub(crate) struct SelectedDistribution {
version: Version,
source: SelectedSource,
}
#[derive(Debug)]
enum SelectedSource {
Url {
dist: ResolvedDist,
metadata_id: DistributionId,
metadata: Metadata,
},
Registry {
dist: ResolvedDist,
metadata_id: DistributionId,
metadata: Option<Metadata>,
},
}
impl SelectedDistribution {
fn new(
package: &ResolutionPackage,
version: Version,
pins: &FilePins<'_>,
index: &InMemoryIndex,
git: &GitResolver,
) -> Result<Self, ResolveError> {
let source = if let Some(url) = &package.url {
let metadata_id = Dist::from_url(package.name.clone(), url.clone())?.distribution_id();
let response = index.distributions().get(&metadata_id).ok_or_else(|| {
ResolveError::UnregisteredTask(format!("{} @ {}", package.name, url.verbatim))
})?;
let MetadataResponse::Found(archive) = &*response else {
return Err(ResolveError::PackageUnavailable(package.name.clone()));
};
SelectedSource::Url {
dist: ResolvedDist::Installable {
dist: Arc::new(Dist::from_url(
package.name.clone(),
url_to_precise(url.clone(), git),
)?),
version: Some(version.clone()),
},
metadata_id,
metadata: archive.metadata.clone(),
}
} else {
let (dist, metadata_id) =
pins.dist_and_id(&package.name, &version).ok_or_else(|| {
ResolveError::UnregisteredTask(format!("{}=={version}", package.name))
})?;
let metadata = index.distributions().get(metadata_id).and_then(|response| {
if let MetadataResponse::Found(archive) = &*response {
Some(archive.metadata.clone())
} else {
None
}
});
SelectedSource::Registry {
dist: dist.clone(),
metadata_id: metadata_id.clone(),
metadata,
}
};
Ok(Self { version, source })
}
pub(crate) fn version(&self) -> &Version {
&self.version
}
pub(crate) fn into_parts(self) -> (Version, ResolvedDist, Option<Metadata>) {
match self.source {
SelectedSource::Url { dist, metadata, .. } => (self.version, dist, Some(metadata)),
SelectedSource::Registry { dist, metadata, .. } => (self.version, dist, metadata),
}
}
fn metadata_id(&self) -> &DistributionId {
match &self.source {
SelectedSource::Url { metadata_id, .. }
| SelectedSource::Registry { metadata_id, .. } => metadata_id,
}
}
pub(crate) fn hashes_id(&self) -> DistributionId {
match &self.source {
SelectedSource::Url { .. } => self.metadata_id().clone(),
SelectedSource::Registry { dist, .. } => dist.distribution_id(),
}
}
}
impl Resolution<'_> {
pub(crate) fn finalize(
self,
index: &InMemoryIndex,
git: &GitResolver,
) -> Result<ResolvedFork, ResolveError> {
let nodes = self
.nodes
.into_iter()
.map(|(package, version)| {
let selected =
SelectedDistribution::new(&package, version, &self.pins, index, git)?;
Ok((package, selected))
})
.collect::<Result<_, ResolveError>>()?;
Ok(ResolvedFork {
nodes,
edges: self.edges,
env: self.env,
})
}
pub(crate) fn trace_resolution(&self) {
if !tracing::enabled!(Level::TRACE) {
return;
}
trace!("Resolution: {:?}", self.env);
for edge in &self.edges {
trace!(
"Resolution edge: {} -> {}",
edge.from
.as_ref()
.map(|node| node.package.name.as_str())
.unwrap_or("ROOT"),
edge.to.package.name,
);
let mut msg = String::new();
write!(
msg,
"{}",
edge.from
.as_ref()
.map_or(&*MIN_VERSION, |node| &node.version)
)
.unwrap();
if let Some(extra) = edge
.from
.as_ref()
.and_then(|node| node.package.kind.extra())
{
write!(msg, " (extra: {extra})").unwrap();
}
if let Some(dev) = edge
.from
.as_ref()
.and_then(|node| node.package.kind.group())
{
write!(msg, " (group: {dev})").unwrap();
}
write!(msg, " -> ").unwrap();
write!(msg, "{}", edge.to.version).unwrap();
if let Some(extra) = edge.to.package.kind.extra() {
write!(msg, " (extra: {extra})").unwrap();
}
if let Some(dev) = edge.to.package.kind.group() {
write!(msg, " (group: {dev})").unwrap();
}
if let Some(marker) = edge.marker.contents() {
write!(msg, " ; {marker}").unwrap();
}
trace!("Resolution edge: {msg}");
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub(crate) struct ResolutionPackage {
pub(crate) name: PackageName,
pub(crate) kind: PackageNodeKind,
pub(crate) url: Option<VerbatimParsedUrl>,
pub(crate) index: Option<IndexUrl>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub(crate) struct ResolutionNode {
pub(crate) package: ResolutionPackage,
pub(crate) version: Version,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub(crate) struct ResolutionDependencyEdge {
pub(crate) from: Option<ResolutionNode>,
pub(crate) to: ResolutionNode,
pub(crate) marker: MarkerTree,
}
impl ResolutionDependencyEdge {
pub(crate) fn universal_marker(&self) -> UniversalMarker {
UniversalMarker::new(self.marker, ConflictMarker::TRUE)
}
}