1use std::collections::hash_map::Entry;
2use std::collections::{HashMap, HashSet};
3use std::path::PathBuf;
4
5use indexmap::IndexMap;
6use semver::{Version, VersionReq};
7
8use super::compat::CompatibilityResult;
9use crate::config::{FilterMode, Manifest, SourceSpec};
10use crate::error::ResolutionError;
11use crate::lock::ItemKind;
12use crate::source::ResolvedRef;
13use crate::types::{ItemName, SourceId, SourceName};
14
15#[derive(Debug, Clone)]
20pub struct ResolvedGraph {
21 pub nodes: IndexMap<SourceName, ResolvedNode>,
22 pub order: Vec<SourceName>,
24 pub filters: HashMap<SourceName, Vec<FilterMode>>,
26 pub version_constraints: HashMap<SourceName, Vec<(String, VersionConstraint)>>,
28}
29
30#[derive(Debug, Clone)]
32pub struct ResolvedNode {
33 pub source_name: SourceName,
34 pub source_id: SourceId,
35 pub rooted_ref: RootedSourceRef,
36 pub resolved_ref: ResolvedRef,
37 pub latest_version: Option<Version>,
38 pub manifest: Option<Manifest>,
40 pub deps: Vec<SourceName>,
42}
43
44#[derive(Debug, Clone)]
46pub struct RootedSourceRef {
47 pub checkout_root: PathBuf,
48 pub package_root: PathBuf,
49}
50
51#[derive(Debug, Clone)]
53pub enum VersionConstraint {
54 Semver(VersionReq),
56 Latest,
58 RefPin(String),
60}
61
62impl std::fmt::Display for VersionConstraint {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 VersionConstraint::Semver(req) => write!(f, "{req}"),
66 VersionConstraint::Latest => write!(f, "latest"),
67 VersionConstraint::RefPin(reference) => write!(f, "ref:{reference}"),
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct PendingItem {
75 pub package: SourceName,
77 pub item: ItemName,
79 pub kind: ItemKind,
81 pub constraint: VersionConstraint,
83 pub required_by: String,
85 pub is_local: bool,
87 pub spec: SourceSpec,
89}
90
91#[derive(Debug)]
93pub enum VersionCheckResult {
94 NotSeen,
96 SameVersion,
98 PotentiallyConflicting {
100 existing: VersionConstraint,
101 requested: VersionConstraint,
102 },
103 DifferentVersion {
105 existing: VersionConstraint,
106 requested: VersionConstraint,
107 },
108}
109
110#[derive(Debug, Clone, Hash, Eq, PartialEq)]
112struct VisitedItem {
113 package: SourceName,
114 item: ItemName,
115}
116
117#[derive(Debug, Clone)]
119pub struct ResolvedVersion {
120 pub constraint: VersionConstraint,
121 pub resolved_ref: ResolvedRef,
122}
123
124pub struct VisitedSet {
126 index: HashMap<(SourceName, ItemName), ResolvedVersion>,
128}
129
130impl Default for VisitedSet {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl VisitedSet {
137 pub fn new() -> Self {
138 Self {
139 index: HashMap::new(),
140 }
141 }
142
143 fn index_key(package: &SourceName, item: &ItemName) -> (SourceName, ItemName) {
144 let key = VisitedItem {
145 package: package.clone(),
146 item: item.clone(),
147 };
148 (key.package, key.item)
149 }
150
151 pub fn check_version(
153 &self,
154 package: &SourceName,
155 item: &ItemName,
156 constraint: &VersionConstraint,
157 ) -> VersionCheckResult {
158 match self.index.get(&Self::index_key(package, item)) {
159 None => VersionCheckResult::NotSeen,
160 Some(existing) => match existing
161 .constraint
162 .compatible_with_resolved(constraint, existing.resolved_ref.version.as_ref())
163 {
164 CompatibilityResult::Compatible => VersionCheckResult::SameVersion,
165 CompatibilityResult::PotentiallyConflicting => {
166 VersionCheckResult::PotentiallyConflicting {
167 existing: existing.constraint.clone(),
168 requested: constraint.clone(),
169 }
170 }
171 CompatibilityResult::Conflicting => VersionCheckResult::DifferentVersion {
172 existing: existing.constraint.clone(),
173 requested: constraint.clone(),
174 },
175 },
176 }
177 }
178
179 pub fn insert(
181 &mut self,
182 package: SourceName,
183 item: ItemName,
184 constraint: VersionConstraint,
185 resolved_ref: ResolvedRef,
186 ) {
187 self.index.insert(
188 Self::index_key(&package, &item),
189 ResolvedVersion {
190 constraint,
191 resolved_ref,
192 },
193 );
194 }
195
196 pub fn iter(&self) -> impl Iterator<Item = (&(SourceName, ItemName), &ResolvedVersion)> {
198 self.index.iter()
199 }
200}
201
202pub struct PackageVersions {
204 versions: HashMap<SourceName, (ResolvedRef, VersionConstraint, String)>,
206}
207
208impl Default for PackageVersions {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214impl PackageVersions {
215 pub fn new() -> Self {
216 Self {
217 versions: HashMap::new(),
218 }
219 }
220
221 pub fn check_or_insert(
223 &mut self,
224 package: &SourceName,
225 resolved: &ResolvedRef,
226 requested: &VersionConstraint,
227 required_by: &str,
228 is_local: bool,
229 ) -> Result<(), ResolutionError> {
230 if is_local {
231 return Ok(());
232 }
233
234 match self.versions.entry(package.clone()) {
235 Entry::Vacant(entry) => {
236 entry.insert((resolved.clone(), requested.clone(), required_by.to_string()));
237 Ok(())
238 }
239 Entry::Occupied(entry) => {
240 let (existing_ref, existing_constraint, existing_by) = entry.get();
241 match existing_constraint.compatible_with_resolved(
242 requested,
243 existing_ref.version.as_ref().or(resolved.version.as_ref()),
244 ) {
245 CompatibilityResult::Compatible
246 | CompatibilityResult::PotentiallyConflicting => {
247 if resolved_ref_matches(existing_ref, resolved) {
248 Ok(())
249 } else {
250 Err(ResolutionError::PackageVersionConflict {
251 package: package.to_string(),
252 existing: format!("{existing_ref:?} (required by {existing_by})"),
253 requested: format!("{resolved:?} (required by {required_by})"),
254 chain: required_by.to_string(),
255 })
256 }
257 }
258 CompatibilityResult::Conflicting => {
259 Err(ResolutionError::PackageVersionConflict {
260 package: package.to_string(),
261 existing: format!("{existing_constraint} (required by {existing_by})"),
262 requested: format!("{requested} (required by {required_by})"),
263 chain: required_by.to_string(),
264 })
265 }
266 }
267 }
268 }
269 }
270}
271
272fn resolved_ref_matches(existing: &ResolvedRef, incoming: &ResolvedRef) -> bool {
273 existing.source_name == incoming.source_name
274 && existing.version == incoming.version
275 && existing.version_tag == incoming.version_tag
276 && existing.commit == incoming.commit
277 && crate::target::paths_equivalent(
278 &existing.tree_path.to_string_lossy(),
279 &incoming.tree_path.to_string_lossy(),
280 )
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
285pub enum ResolveMode {
286 Sync,
288 Frozen,
290 Upgrade {
292 targets: HashSet<SourceName>,
294 bump_direct_constraints: bool,
296 },
297}
298
299#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct ResolveOptions {
302 pub mode: ResolveMode,
303}
304
305impl Default for ResolveOptions {
306 fn default() -> Self {
307 Self {
308 mode: ResolveMode::Sync,
309 }
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub(crate) enum VersionSelectionPolicy {
316 PreferLockThenLatest,
318 LatestOnly,
320 LockOnly,
322}
323
324impl ResolveOptions {
325 pub fn sync() -> Self {
326 Self {
327 mode: ResolveMode::Sync,
328 }
329 }
330
331 pub fn frozen() -> Self {
332 Self {
333 mode: ResolveMode::Frozen,
334 }
335 }
336
337 pub fn upgrade(targets: HashSet<SourceName>, bump_direct_constraints: bool) -> Self {
338 Self {
339 mode: ResolveMode::Upgrade {
340 targets,
341 bump_direct_constraints,
342 },
343 }
344 }
345
346 pub(crate) fn direct_constraint_for(
347 &self,
348 source_name: &SourceName,
349 declared: VersionConstraint,
350 ) -> VersionConstraint {
351 if matches!(
352 &self.mode,
353 ResolveMode::Upgrade {
354 bump_direct_constraints: true,
355 ..
356 }
357 ) && self.is_upgrade_target(source_name)
358 {
359 VersionConstraint::Latest
360 } else {
361 declared
362 }
363 }
364
365 pub(crate) fn is_upgrade_target(&self, source_name: &SourceName) -> bool {
366 match &self.mode {
367 ResolveMode::Upgrade { targets, .. } => {
368 targets.is_empty() || targets.contains(source_name)
369 }
370 ResolveMode::Sync | ResolveMode::Frozen => false,
371 }
372 }
373
374 pub(crate) fn version_selection_policy(
375 &self,
376 source_name: &SourceName,
377 ) -> VersionSelectionPolicy {
378 match &self.mode {
379 ResolveMode::Frozen => VersionSelectionPolicy::LockOnly,
380 ResolveMode::Upgrade { .. } if self.is_upgrade_target(source_name) => {
381 VersionSelectionPolicy::LatestOnly
382 }
383 ResolveMode::Sync | ResolveMode::Upgrade { .. } => {
384 VersionSelectionPolicy::PreferLockThenLatest
385 }
386 }
387 }
388}