runmat_package/resolve/
candidate.rs1use super::ResolutionRequirement;
2use crate::{
3 CanonicalPackageId, HostCapability, PackageInstanceId, RegistryId, ResolveError,
4 TargetPredicate,
5};
6use semver::VersionReq;
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9use std::future::Future;
10use std::pin::Pin;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct CandidateMetadata {
14 pub instance: PackageInstanceId,
15 pub dependencies: Vec<ResolutionRequirement>,
16 pub features: BTreeMap<String, BTreeSet<String>>,
17 pub required_capabilities: BTreeSet<HostCapability>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub runmat_version: Option<VersionReq>,
20 pub singleton: bool,
21 pub yanked: bool,
22 pub available_offline: bool,
23 pub target_artifacts: BTreeSet<TargetPredicate>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub registry_metadata: Option<crate::RegistryReleaseMetadata>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct CandidateQuery {
30 pub package: CanonicalPackageId,
31 pub source_registry: RegistryId,
32 pub offline: bool,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Default)]
36pub struct SourceSelectionPolicy {
37 pub replacements: BTreeMap<RegistryId, RegistryId>,
38 pub offline: bool,
39}
40
41pub trait CandidateProvider {
42 fn candidates<'a>(
43 &'a self,
44 query: &'a CandidateQuery,
45 ) -> Pin<Box<dyn Future<Output = Result<Vec<CandidateMetadata>, ResolveError>> + 'a>>;
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Default)]
49pub struct CandidateIndex {
50 packages: BTreeMap<CanonicalPackageId, Vec<CandidateMetadata>>,
51}
52
53impl CandidateIndex {
54 pub fn insert(&mut self, candidate: CandidateMetadata) {
55 self.packages
56 .entry(candidate.instance.package.clone())
57 .or_default()
58 .push(candidate);
59 }
60
61 pub fn candidates(&self, package: &CanonicalPackageId) -> &[CandidateMetadata] {
62 self.packages.get(package).map(Vec::as_slice).unwrap_or(&[])
63 }
64
65 pub fn package_ids(&self) -> impl Iterator<Item = &CanonicalPackageId> {
66 self.packages.keys()
67 }
68}
69
70pub async fn acquire_candidates(
71 provider: &dyn CandidateProvider,
72 packages: impl IntoIterator<Item = CanonicalPackageId>,
73 offline: bool,
74) -> Result<CandidateIndex, ResolveError> {
75 acquire_candidates_with_policy(
76 provider,
77 packages,
78 &SourceSelectionPolicy {
79 replacements: BTreeMap::new(),
80 offline,
81 },
82 )
83 .await
84}
85
86pub async fn acquire_candidates_with_policy(
87 provider: &dyn CandidateProvider,
88 packages: impl IntoIterator<Item = CanonicalPackageId>,
89 policy: &SourceSelectionPolicy,
90) -> Result<CandidateIndex, ResolveError> {
91 let mut index = CandidateIndex::default();
92 let mut pending = packages.into_iter().collect::<BTreeSet<_>>();
93 let mut queried = BTreeSet::new();
94 while let Some(package) = pending.pop_first() {
95 if !queried.insert(package.clone()) {
96 continue;
97 }
98 let source_registry = replacement_registry(package.registry(), &policy.replacements)?;
99 let mut candidates = provider
100 .candidates(&CandidateQuery {
101 package: package.clone(),
102 source_registry,
103 offline: policy.offline,
104 })
105 .await?;
106 candidates.sort_by(|left, right| {
107 right
108 .instance
109 .version
110 .cmp(&left.instance.version)
111 .then_with(|| {
112 left.instance
113 .identity_digest
114 .cmp(&right.instance.identity_digest)
115 })
116 });
117 for candidate in candidates {
118 if candidate.instance.package != package {
119 return Err(ResolveError::Provider(format!(
120 "provider returned {} for query {package}",
121 candidate.instance.package
122 )));
123 }
124 pending.extend(
125 candidate
126 .dependencies
127 .iter()
128 .map(|dependency| dependency.package.clone()),
129 );
130 index.insert(candidate);
131 }
132 }
133 Ok(index)
134}
135
136fn replacement_registry(
137 source: &RegistryId,
138 replacements: &BTreeMap<RegistryId, RegistryId>,
139) -> Result<RegistryId, ResolveError> {
140 let mut current = source.clone();
141 let mut visited = BTreeSet::new();
142 while let Some(replacement) = replacements.get(¤t) {
143 if !visited.insert(current.clone()) {
144 return Err(ResolveError::Provider(format!(
145 "registry source replacement cycle begins at `{source}`"
146 )));
147 }
148 current = replacement.clone();
149 }
150 Ok(current)
151}