1use std::collections::hash_map::Entry;
2use std::collections::{HashMap, HashSet};
3use std::path::PathBuf;
4
5use indexmap::IndexMap;
6use semver::VersionReq;
7
8use super::compat::CompatibilityResult;
9use crate::config::{FilterMode, Manifest};
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 pub unreadable_hook_surfaces:
33 std::collections::BTreeMap<SourceName, std::collections::BTreeSet<String>>,
34}
35
36#[derive(Debug, Clone)]
38pub struct ResolvedNode {
39 pub source_name: SourceName,
40 pub source_id: SourceId,
41 pub rooted_ref: RootedSourceRef,
42 pub resolved_ref: ResolvedRef,
43 pub manifest: Option<Manifest>,
45 pub deps: Vec<SourceName>,
47}
48
49#[derive(Debug, Clone)]
51pub struct RootedSourceRef {
52 pub checkout_root: PathBuf,
53 pub package_root: PathBuf,
54}
55
56#[derive(Debug, Clone)]
58pub enum VersionConstraint {
59 Semver(VersionReq),
61 Latest,
63 RefPin(String),
65}
66
67impl std::fmt::Display for VersionConstraint {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 VersionConstraint::Semver(req) => write!(f, "{req}"),
71 VersionConstraint::Latest => write!(f, "latest"),
72 VersionConstraint::RefPin(reference) => write!(f, "ref:{reference}"),
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
79pub struct PendingItem {
80 pub package: SourceName,
82 pub item: ItemName,
84 pub kind: ItemKind,
86 pub constraint: VersionConstraint,
88 pub required_by: String,
90 pub is_local: bool,
92}
93
94#[derive(Debug)]
96pub enum VersionCheckResult {
97 NotSeen,
99 SameVersion,
101 PotentiallyConflicting {
103 existing: VersionConstraint,
104 requested: VersionConstraint,
105 },
106 DifferentVersion {
108 existing: VersionConstraint,
109 requested: VersionConstraint,
110 },
111}
112
113#[derive(Debug, Clone, Hash, Eq, PartialEq)]
115struct VisitedItem {
116 package: SourceName,
117 item: ItemName,
118}
119
120#[derive(Debug, Clone)]
122pub struct ResolvedVersion {
123 pub constraint: VersionConstraint,
124 pub resolved_ref: ResolvedRef,
125}
126
127pub struct VisitedSet {
129 index: HashMap<(SourceName, ItemName), ResolvedVersion>,
131}
132
133impl Default for VisitedSet {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl VisitedSet {
140 pub fn new() -> Self {
141 Self {
142 index: HashMap::new(),
143 }
144 }
145
146 fn index_key(package: &SourceName, item: &ItemName) -> (SourceName, ItemName) {
147 let key = VisitedItem {
148 package: package.clone(),
149 item: item.clone(),
150 };
151 (key.package, key.item)
152 }
153
154 pub fn check_version(
156 &self,
157 package: &SourceName,
158 item: &ItemName,
159 constraint: &VersionConstraint,
160 ) -> VersionCheckResult {
161 match self.index.get(&Self::index_key(package, item)) {
162 None => VersionCheckResult::NotSeen,
163 Some(existing) => match existing
164 .constraint
165 .compatible_with_resolved(constraint, existing.resolved_ref.version.as_ref())
166 {
167 CompatibilityResult::Compatible => VersionCheckResult::SameVersion,
168 CompatibilityResult::PotentiallyConflicting => {
169 VersionCheckResult::PotentiallyConflicting {
170 existing: existing.constraint.clone(),
171 requested: constraint.clone(),
172 }
173 }
174 CompatibilityResult::Conflicting => VersionCheckResult::DifferentVersion {
175 existing: existing.constraint.clone(),
176 requested: constraint.clone(),
177 },
178 },
179 }
180 }
181
182 pub fn insert(
184 &mut self,
185 package: SourceName,
186 item: ItemName,
187 constraint: VersionConstraint,
188 resolved_ref: ResolvedRef,
189 ) {
190 self.index.insert(
191 Self::index_key(&package, &item),
192 ResolvedVersion {
193 constraint,
194 resolved_ref,
195 },
196 );
197 }
198}
199
200pub struct PackageVersions {
202 versions: HashMap<SourceName, (ResolvedRef, VersionConstraint, String)>,
204}
205
206impl Default for PackageVersions {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212impl PackageVersions {
213 pub fn new() -> Self {
214 Self {
215 versions: HashMap::new(),
216 }
217 }
218
219 pub fn check_or_insert(
221 &mut self,
222 package: &SourceName,
223 resolved: &ResolvedRef,
224 requested: &VersionConstraint,
225 required_by: &str,
226 is_local: bool,
227 ) -> Result<(), ResolutionError> {
228 if is_local {
229 return Ok(());
230 }
231
232 match self.versions.entry(package.clone()) {
233 Entry::Vacant(entry) => {
234 entry.insert((resolved.clone(), requested.clone(), required_by.to_string()));
235 Ok(())
236 }
237 Entry::Occupied(entry) => {
238 let (existing_ref, existing_constraint, existing_by) = entry.get();
239 match existing_constraint.compatible_with_resolved(
240 requested,
241 existing_ref.version.as_ref().or(resolved.version.as_ref()),
242 ) {
243 CompatibilityResult::Compatible
244 | CompatibilityResult::PotentiallyConflicting => {
245 if resolved_ref_matches(existing_ref, resolved) {
246 Ok(())
247 } else {
248 Err(ResolutionError::PackageVersionConflict {
249 package: package.to_string(),
250 existing: format!("{existing_ref:?} (required by {existing_by})"),
251 requested: format!("{resolved:?} (required by {required_by})"),
252 chain: required_by.to_string(),
253 })
254 }
255 }
256 CompatibilityResult::Conflicting => {
257 Err(ResolutionError::PackageVersionConflict {
258 package: package.to_string(),
259 existing: format!("{existing_constraint} (required by {existing_by})"),
260 requested: format!("{requested} (required by {required_by})"),
261 chain: required_by.to_string(),
262 })
263 }
264 }
265 }
266 }
267 }
268}
269
270fn resolved_ref_matches(existing: &ResolvedRef, incoming: &ResolvedRef) -> bool {
271 existing.source_name == incoming.source_name
272 && existing.version == incoming.version
273 && existing.version_tag == incoming.version_tag
274 && existing.commit == incoming.commit
275 && crate::target::paths_equivalent(
276 &existing.tree_path.to_string_lossy(),
277 &incoming.tree_path.to_string_lossy(),
278 )
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
283pub enum ResolveMode {
284 Sync,
286 Frozen,
288 Upgrade {
290 targets: HashSet<SourceName>,
292 bump_direct_constraints: bool,
294 },
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
299pub struct ResolveOptions {
300 pub mode: ResolveMode,
301 pub staging_root: Option<std::path::PathBuf>,
303 pub(crate) source_overrides: indexmap::IndexMap<SourceName, std::path::PathBuf>,
305}
306
307impl Default for ResolveOptions {
308 fn default() -> Self {
309 Self {
310 mode: ResolveMode::Sync,
311 staging_root: None,
312 source_overrides: indexmap::IndexMap::new(),
313 }
314 }
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub(crate) enum VersionSelectionPolicy {
320 PreferLockThenLatest,
322 LatestOnly,
324 LockOnly,
326}
327
328impl ResolveOptions {
329 pub fn sync() -> Self {
330 Self {
331 mode: ResolveMode::Sync,
332 staging_root: None,
333 source_overrides: indexmap::IndexMap::new(),
334 }
335 }
336
337 pub fn frozen() -> Self {
338 Self {
339 mode: ResolveMode::Frozen,
340 staging_root: None,
341 source_overrides: indexmap::IndexMap::new(),
342 }
343 }
344
345 pub fn upgrade(targets: HashSet<SourceName>, bump_direct_constraints: bool) -> Self {
346 Self {
347 mode: ResolveMode::Upgrade {
348 targets,
349 bump_direct_constraints,
350 },
351 staging_root: None,
352 source_overrides: indexmap::IndexMap::new(),
353 }
354 }
355
356 pub fn with_staging_root(mut self, staging_root: std::path::PathBuf) -> Self {
357 self.staging_root = Some(staging_root);
358 self
359 }
360
361 pub(crate) fn with_source_overrides(
362 mut self,
363 source_overrides: indexmap::IndexMap<SourceName, std::path::PathBuf>,
364 ) -> Self {
365 self.source_overrides = source_overrides;
366 self
367 }
368
369 pub(crate) fn direct_constraint_for(
370 &self,
371 source_name: &SourceName,
372 declared: VersionConstraint,
373 ) -> VersionConstraint {
374 if matches!(
375 &self.mode,
376 ResolveMode::Upgrade {
377 bump_direct_constraints: true,
378 ..
379 }
380 ) && self.is_upgrade_target(source_name)
381 {
382 VersionConstraint::Latest
383 } else {
384 declared
385 }
386 }
387
388 pub(crate) fn is_upgrade_target(&self, source_name: &SourceName) -> bool {
389 match &self.mode {
390 ResolveMode::Upgrade { targets, .. } => {
391 targets.is_empty() || targets.contains(source_name)
392 }
393 ResolveMode::Sync | ResolveMode::Frozen => false,
394 }
395 }
396
397 pub(crate) fn version_selection_policy(
398 &self,
399 source_name: &SourceName,
400 ) -> VersionSelectionPolicy {
401 match &self.mode {
402 ResolveMode::Frozen => VersionSelectionPolicy::LockOnly,
403 ResolveMode::Upgrade { .. } if self.is_upgrade_target(source_name) => {
404 VersionSelectionPolicy::LatestOnly
405 }
406 ResolveMode::Sync | ResolveMode::Upgrade { .. } => {
407 VersionSelectionPolicy::PreferLockThenLatest
408 }
409 }
410 }
411}