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 pub staging_root: Option<std::path::PathBuf>,
305}
306
307impl Default for ResolveOptions {
308 fn default() -> Self {
309 Self {
310 mode: ResolveMode::Sync,
311 staging_root: None,
312 }
313 }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub(crate) enum VersionSelectionPolicy {
319 PreferLockThenLatest,
321 LatestOnly,
323 LockOnly,
325}
326
327impl ResolveOptions {
328 pub fn sync() -> Self {
329 Self {
330 mode: ResolveMode::Sync,
331 staging_root: None,
332 }
333 }
334
335 pub fn frozen() -> Self {
336 Self {
337 mode: ResolveMode::Frozen,
338 staging_root: None,
339 }
340 }
341
342 pub fn upgrade(targets: HashSet<SourceName>, bump_direct_constraints: bool) -> Self {
343 Self {
344 mode: ResolveMode::Upgrade {
345 targets,
346 bump_direct_constraints,
347 },
348 staging_root: None,
349 }
350 }
351
352 pub fn with_staging_root(mut self, staging_root: std::path::PathBuf) -> Self {
353 self.staging_root = Some(staging_root);
354 self
355 }
356
357 pub(crate) fn direct_constraint_for(
358 &self,
359 source_name: &SourceName,
360 declared: VersionConstraint,
361 ) -> VersionConstraint {
362 if matches!(
363 &self.mode,
364 ResolveMode::Upgrade {
365 bump_direct_constraints: true,
366 ..
367 }
368 ) && self.is_upgrade_target(source_name)
369 {
370 VersionConstraint::Latest
371 } else {
372 declared
373 }
374 }
375
376 pub(crate) fn is_upgrade_target(&self, source_name: &SourceName) -> bool {
377 match &self.mode {
378 ResolveMode::Upgrade { targets, .. } => {
379 targets.is_empty() || targets.contains(source_name)
380 }
381 ResolveMode::Sync | ResolveMode::Frozen => false,
382 }
383 }
384
385 pub(crate) fn version_selection_policy(
386 &self,
387 source_name: &SourceName,
388 ) -> VersionSelectionPolicy {
389 match &self.mode {
390 ResolveMode::Frozen => VersionSelectionPolicy::LockOnly,
391 ResolveMode::Upgrade { .. } if self.is_upgrade_target(source_name) => {
392 VersionSelectionPolicy::LatestOnly
393 }
394 ResolveMode::Sync | ResolveMode::Upgrade { .. } => {
395 VersionSelectionPolicy::PreferLockThenLatest
396 }
397 }
398 }
399}