1use crate::{
2 APPLICATION_MODULE_LOCK_PROTOCOL, ApplicationModuleLock, DesiredModuleComposition,
3 DesiredModuleSelection, LockedCapabilityBinding, LockedConsoleUiArtifact, LockedModule,
4 LockedModuleReason, ManagedDeliveryKind, ModuleRootChange,
5};
6use lenso_contracts::{
7 ModuleDelivery, ModuleEligibility, ModuleEligibilityState, ModuleRelease,
8 ModuleVerificationCell, digest_json,
9};
10use schemars::JsonSchema;
11use semver::{Version, VersionReq};
12use serde::{Deserialize, Serialize};
13use std::cmp::Ordering;
14use std::collections::{BTreeMap, BTreeSet};
15
16pub const MODULE_RESOLUTION_CONFLICT_PROTOCOL: &str = "lenso.module-resolution-conflict.v1";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
19#[serde(deny_unknown_fields)]
20pub struct ModuleResolutionCandidate {
21 pub catalog_snapshot_digest: String,
22 pub release_digest: String,
23 pub release: ModuleRelease,
24 pub eligibility: ModuleEligibility,
25 pub verification_cell: ModuleVerificationCell,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ModuleResolutionRequest {
30 pub current_desired: DesiredModuleComposition,
31 pub current_lock: Option<ApplicationModuleLock>,
32 pub change: ModuleRootChange,
33 pub catalog_snapshot_digest: String,
34 pub trust_policy_digest: String,
35 pub resolver_version: String,
36 pub candidates: Vec<ModuleResolutionCandidate>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ModuleResolution {
41 pub target_desired: DesiredModuleComposition,
42 pub target_lock: ApplicationModuleLock,
43 pub removed_orphan_module_ids: Vec<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
47#[serde(deny_unknown_fields)]
48pub struct ModuleResolutionConflict {
49 pub protocol: String,
50 pub code: String,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub module_id: Option<String>,
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
54 pub dependency_paths: Vec<Vec<String>>,
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
56 pub constraints: Vec<String>,
57 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub eligible_alternatives: Vec<String>,
59}
60
61#[derive(Debug, thiserror::Error)]
62#[error("Module graph resolution failed: {conflict:?}")]
63pub struct ModuleResolutionError {
64 pub conflict: Box<ModuleResolutionConflict>,
65}
66
67#[derive(Debug, Default, Clone, Copy)]
68pub struct ModuleGraphResolver;
69
70#[derive(Debug, Clone)]
71struct RequirementConstraint {
72 requirement: VersionReq,
73 requirement_text: String,
74 capabilities: Vec<String>,
75 exact_release_digest: Option<String>,
76 delivery: Option<ManagedDeliveryKind>,
77 path: Vec<String>,
78}
79
80#[derive(Debug, Clone, Default)]
81struct SolverState {
82 constraints: BTreeMap<String, Vec<RequirementConstraint>>,
83 selections: BTreeMap<String, usize>,
84 dependencies: BTreeMap<String, BTreeSet<String>>,
85}
86
87impl ModuleGraphResolver {
88 pub fn resolve(
89 &self,
90 request: &ModuleResolutionRequest,
91 ) -> Result<ModuleResolution, ModuleResolutionError> {
92 let mut target_desired = request.current_desired.clone();
93 apply_root_change(&mut target_desired, &request.change)?;
94 normalize_desired(&mut target_desired);
95
96 let mut candidates = request.candidates.clone();
97 validate_candidates(&request.catalog_snapshot_digest, &candidates)?;
98 candidates.sort_by(|left, right| {
99 left.release
100 .module_id
101 .cmp(&right.release.module_id)
102 .then_with(|| left.release_digest.cmp(&right.release_digest))
103 });
104
105 let selected_optional = target_desired
106 .selected
107 .iter()
108 .map(|selection| {
109 (
110 selection.module_id.clone(),
111 selection
112 .optional_requirements
113 .iter()
114 .cloned()
115 .collect::<BTreeSet<_>>(),
116 )
117 })
118 .collect::<BTreeMap<_, _>>();
119 let direct_ids = target_desired
120 .selected
121 .iter()
122 .map(|selection| selection.module_id.clone())
123 .collect::<BTreeSet<_>>();
124 let current_locked = request
125 .current_lock
126 .as_ref()
127 .map(|lock| {
128 lock.modules
129 .iter()
130 .map(|module| (module.module_id.clone(), module.release_digest.clone()))
131 .collect::<BTreeMap<_, _>>()
132 })
133 .unwrap_or_default();
134
135 let mut state = SolverState::default();
136 for selection in &target_desired.selected {
137 let requirement = parse_requirement(
138 &selection.module_id,
139 &selection.version_requirement,
140 vec![selection.module_id.clone()],
141 )?;
142 state
143 .constraints
144 .entry(selection.module_id.clone())
145 .or_default()
146 .push(RequirementConstraint {
147 requirement,
148 requirement_text: selection.version_requirement.clone(),
149 capabilities: Vec::new(),
150 exact_release_digest: selection.exact_release_digest.clone(),
151 delivery: selection.delivery_preference,
152 path: vec![selection.module_id.clone()],
153 });
154 }
155
156 let solved = solve(state, &candidates, &selected_optional, ¤t_locked)?;
157 validate_optional_selections(&target_desired, &candidates, &solved)?;
158 let desired_digest = digest_json(&target_desired)
159 .map_err(|_| conflict("desired_composition_not_canonical", None))?;
160 let target_lock = build_lock(
161 request,
162 &target_desired,
163 &desired_digest,
164 &candidates,
165 &solved,
166 &direct_ids,
167 &selected_optional,
168 );
169 let target_ids = target_lock
170 .modules
171 .iter()
172 .map(|module| module.module_id.clone())
173 .collect::<BTreeSet<_>>();
174 let mut removed_orphan_module_ids = request
175 .current_lock
176 .as_ref()
177 .into_iter()
178 .flat_map(|lock| lock.modules.iter())
179 .filter(|module| !target_ids.contains(&module.module_id))
180 .map(|module| module.module_id.clone())
181 .collect::<Vec<_>>();
182 removed_orphan_module_ids.sort();
183
184 Ok(ModuleResolution {
185 target_desired,
186 target_lock,
187 removed_orphan_module_ids,
188 })
189 }
190}
191
192fn apply_root_change(
193 desired: &mut DesiredModuleComposition,
194 change: &ModuleRootChange,
195) -> Result<(), ModuleResolutionError> {
196 let before = desired.clone();
197 match change {
198 ModuleRootChange::Install { selection } => {
199 if desired
200 .selected
201 .iter()
202 .any(|current| current.module_id == selection.module_id)
203 {
204 return Err(conflict(
205 "module_already_selected",
206 Some(&selection.module_id),
207 ));
208 }
209 desired.selected.push(selection.clone());
210 }
211 ModuleRootChange::Update {
212 module_id,
213 version_requirement,
214 } => find_selection_mut(desired, module_id)?
215 .version_requirement
216 .clone_from(version_requirement),
217 ModuleRootChange::Uninstall { module_id } => {
218 let old_len = desired.selected.len();
219 desired.selected.retain(|item| item.module_id != *module_id);
220 if desired.selected.len() == old_len {
221 return Err(conflict("module_not_selected", Some(module_id)));
222 }
223 }
224 ModuleRootChange::SelectOptional {
225 module_id,
226 requirement,
227 selected,
228 } => {
229 let selection = find_selection_mut(desired, module_id)?;
230 if *selected {
231 selection.optional_requirements.push(requirement.clone());
232 } else {
233 selection
234 .optional_requirements
235 .retain(|item| item != requirement);
236 }
237 }
238 ModuleRootChange::SwitchDelivery {
239 module_id,
240 delivery,
241 } => {
242 find_selection_mut(desired, module_id)?.delivery_preference = Some(*delivery);
243 }
244 ModuleRootChange::Restore { .. } | ModuleRootChange::Repair { .. } => {
245 return Err(conflict("change_requires_exact_lock", None));
246 }
247 }
248 if *desired != before {
249 desired.revision = desired.revision.saturating_add(1);
250 }
251 Ok(())
252}
253
254fn find_selection_mut<'a>(
255 desired: &'a mut DesiredModuleComposition,
256 module_id: &str,
257) -> Result<&'a mut DesiredModuleSelection, ModuleResolutionError> {
258 desired
259 .selected
260 .iter_mut()
261 .find(|selection| selection.module_id == module_id)
262 .ok_or_else(|| conflict("module_not_selected", Some(module_id)))
263}
264
265fn normalize_desired(desired: &mut DesiredModuleComposition) {
266 for selection in &mut desired.selected {
267 selection.optional_requirements.sort();
268 selection.optional_requirements.dedup();
269 }
270 desired
271 .selected
272 .sort_by(|left, right| left.module_id.cmp(&right.module_id));
273 desired
274 .local_overrides
275 .sort_by(|left, right| left.module_id.cmp(&right.module_id));
276}
277
278fn validate_candidates(
279 snapshot_digest: &str,
280 candidates: &[ModuleResolutionCandidate],
281) -> Result<(), ModuleResolutionError> {
282 let mut identities = BTreeSet::new();
283 for candidate in candidates {
284 let release = &candidate.release;
285 if candidate.catalog_snapshot_digest != snapshot_digest {
286 return Err(conflict(
287 "candidate_snapshot_mismatch",
288 Some(&release.module_id),
289 ));
290 }
291 if digest_json(release).ok().as_deref() != Some(candidate.release_digest.as_str()) {
292 return Err(conflict(
293 "candidate_release_digest_mismatch",
294 Some(&release.module_id),
295 ));
296 }
297 if !release.validate().is_empty() {
298 return Err(conflict(
299 "candidate_release_invalid",
300 Some(&release.module_id),
301 ));
302 }
303 if candidate.verification_cell.module_release_digest != candidate.release_digest {
304 return Err(conflict(
305 "candidate_verification_cell_mismatch",
306 Some(&release.module_id),
307 ));
308 }
309 let identity = (
310 release.module_id.clone(),
311 release.version.clone(),
312 ManagedDeliveryKind::from(&release.delivery),
313 );
314 if !identities.insert(identity) {
315 return Err(conflict(
316 "duplicate_release_identity",
317 Some(&release.module_id),
318 ));
319 }
320 }
321 Ok(())
322}
323
324#[allow(clippy::too_many_lines)]
325fn solve(
326 state: SolverState,
327 candidates: &[ModuleResolutionCandidate],
328 selected_optional: &BTreeMap<String, BTreeSet<String>>,
329 current_locked: &BTreeMap<String, String>,
330) -> Result<SolverState, ModuleResolutionError> {
331 if let Some((selected_id, _selected_index)) = state
332 .selections
333 .iter()
334 .find(|(id, index)| !candidate_matches(&candidates[**index], &state.constraints[*id]))
335 {
336 return Err(unsatisfied_conflict(
337 selected_id,
338 &state.constraints[selected_id],
339 candidates,
340 ));
341 }
342
343 let unresolved = state
344 .constraints
345 .iter()
346 .filter(|(module_id, _)| !state.selections.contains_key(*module_id))
347 .map(|(module_id, constraints)| {
348 let options = ranked_options(module_id, constraints, candidates, current_locked);
349 (module_id.clone(), options)
350 })
351 .min_by(|left, right| {
352 left.1
353 .len()
354 .cmp(&right.1.len())
355 .then_with(|| left.0.cmp(&right.0))
356 });
357 let Some((module_id, options)) = unresolved else {
358 return Ok(state);
359 };
360 if options.is_empty() {
361 return Err(unsatisfied_conflict(
362 &module_id,
363 &state.constraints[&module_id],
364 candidates,
365 ));
366 }
367
368 let mut first_error = None;
369 for candidate_index in options {
370 let candidate = &candidates[candidate_index];
371 let mut next = state.clone();
372 let mut invalid_candidate = false;
373 next.selections.insert(module_id.clone(), candidate_index);
374 let parent_path = next.constraints[&module_id]
375 .iter()
376 .map(|constraint| constraint.path.clone())
377 .min()
378 .unwrap_or_else(|| vec![module_id.clone()]);
379 for requirement in candidate
380 .release
381 .manifest
382 .requires
383 .iter()
384 .filter(|requirement| {
385 !requirement.optional
386 || selected_optional
387 .get(&module_id)
388 .is_some_and(|selected| selected.contains(&requirement.module_id))
389 })
390 {
391 if path_exists(&next.dependencies, &requirement.module_id, &module_id) {
392 let mut path = parent_path.clone();
393 path.push(requirement.module_id.clone());
394 let mut error = conflict("dependency_cycle", Some(&requirement.module_id));
395 error.conflict.dependency_paths.push(path);
396 first_error.get_or_insert(error);
397 invalid_candidate = true;
398 continue;
399 }
400 next.dependencies
401 .entry(module_id.clone())
402 .or_default()
403 .insert(requirement.module_id.clone());
404 let mut path = parent_path.clone();
405 path.push(requirement.module_id.clone());
406 let parsed = match parse_requirement(
407 &requirement.module_id,
408 &requirement.version_requirement,
409 path.clone(),
410 ) {
411 Ok(parsed) => parsed,
412 Err(error) => {
413 first_error.get_or_insert(error);
414 invalid_candidate = true;
415 continue;
416 }
417 };
418 next.constraints
419 .entry(requirement.module_id.clone())
420 .or_default()
421 .push(RequirementConstraint {
422 requirement: parsed,
423 requirement_text: requirement.version_requirement.clone(),
424 capabilities: requirement.capabilities.clone(),
425 exact_release_digest: None,
426 delivery: None,
427 path,
428 });
429 }
430 if invalid_candidate {
431 continue;
432 }
433 match solve(next, candidates, selected_optional, current_locked) {
434 Ok(solved) => return Ok(solved),
435 Err(error) => first_error.get_or_insert(error),
436 };
437 }
438 Err(first_error.unwrap_or_else(|| {
439 unsatisfied_conflict(&module_id, &state.constraints[&module_id], candidates)
440 }))
441}
442
443fn path_exists(graph: &BTreeMap<String, BTreeSet<String>>, start: &str, target: &str) -> bool {
444 if start == target {
445 return true;
446 }
447 graph.get(start).is_some_and(|children| {
448 children
449 .iter()
450 .any(|child| path_exists(graph, child, target))
451 })
452}
453
454fn ranked_options(
455 module_id: &str,
456 constraints: &[RequirementConstraint],
457 candidates: &[ModuleResolutionCandidate],
458 current_locked: &BTreeMap<String, String>,
459) -> Vec<usize> {
460 let mut options = candidates
461 .iter()
462 .enumerate()
463 .filter(|(_, candidate)| {
464 candidate.release.module_id == module_id && candidate_matches(candidate, constraints)
465 })
466 .map(|(index, _)| index)
467 .collect::<Vec<_>>();
468 options.sort_by(|left, right| {
469 compare_candidates(
470 &candidates[*left],
471 &candidates[*right],
472 current_locked.get(module_id).map(String::as_str),
473 )
474 });
475 options
476}
477
478fn compare_candidates(
479 left: &ModuleResolutionCandidate,
480 right: &ModuleResolutionCandidate,
481 current_digest: Option<&str>,
482) -> Ordering {
483 let left_current = current_digest == Some(left.release_digest.as_str());
484 let right_current = current_digest == Some(right.release_digest.as_str());
485 right_current
486 .cmp(&left_current)
487 .then_with(|| {
488 let left_linked = matches!(left.release.delivery, ModuleDelivery::Linked(_));
489 let right_linked = matches!(right.release.delivery, ModuleDelivery::Linked(_));
490 right_linked.cmp(&left_linked)
491 })
492 .then_with(|| {
493 Version::parse(&right.release.version)
494 .expect("validated release version")
495 .cmp(&Version::parse(&left.release.version).expect("validated release version"))
496 })
497 .then_with(|| left.release_digest.cmp(&right.release_digest))
498}
499
500fn candidate_matches(
501 candidate: &ModuleResolutionCandidate,
502 constraints: &[RequirementConstraint],
503) -> bool {
504 if !matches!(
505 candidate.eligibility.state,
506 ModuleEligibilityState::Eligible | ModuleEligibilityState::EligibleWithWarning
507 ) {
508 return false;
509 }
510 let Ok(version) = Version::parse(&candidate.release.version) else {
511 return false;
512 };
513 constraints.iter().all(|constraint| {
514 constraint.requirement.matches(&version)
515 && constraint
516 .exact_release_digest
517 .as_deref()
518 .is_none_or(|digest| digest == candidate.release_digest)
519 && constraint.delivery.is_none_or(|delivery| {
520 delivery == ManagedDeliveryKind::from(&candidate.release.delivery)
521 })
522 && constraint
523 .capabilities
524 .iter()
525 .all(|capability| candidate.release.manifest.capabilities.contains(capability))
526 })
527}
528
529fn parse_requirement(
530 module_id: &str,
531 requirement: &str,
532 path: Vec<String>,
533) -> Result<VersionReq, ModuleResolutionError> {
534 VersionReq::parse(requirement).map_err(|_| {
535 let mut error = conflict("invalid_version_requirement", Some(module_id));
536 error.conflict.constraints.push(requirement.to_owned());
537 error.conflict.dependency_paths.push(path);
538 error
539 })
540}
541
542fn unsatisfied_conflict(
543 module_id: &str,
544 constraints: &[RequirementConstraint],
545 candidates: &[ModuleResolutionCandidate],
546) -> ModuleResolutionError {
547 let mut error = conflict("unsatisfiable_module", Some(module_id));
548 error.conflict.dependency_paths = constraints
549 .iter()
550 .map(|constraint| constraint.path.clone())
551 .collect();
552 error.conflict.dependency_paths.sort();
553 error.conflict.dependency_paths.dedup();
554 error.conflict.constraints = constraints
555 .iter()
556 .flat_map(|constraint| {
557 let mut values = vec![constraint.requirement_text.clone()];
558 values.extend(
559 constraint
560 .capabilities
561 .iter()
562 .map(|capability| format!("capability:{capability}")),
563 );
564 if let Some(digest) = &constraint.exact_release_digest {
565 values.push(format!("release:{digest}"));
566 }
567 if let Some(delivery) = constraint.delivery {
568 values.push(format!("delivery:{delivery:?}"));
569 }
570 values
571 })
572 .collect();
573 error.conflict.constraints.sort();
574 error.conflict.constraints.dedup();
575 error.conflict.eligible_alternatives = candidates
576 .iter()
577 .filter(|candidate| {
578 candidate.release.module_id == module_id
579 && matches!(
580 candidate.eligibility.state,
581 ModuleEligibilityState::Eligible | ModuleEligibilityState::EligibleWithWarning
582 )
583 })
584 .map(|candidate| {
585 format!(
586 "{}@{}#{:?}:{}",
587 candidate.release.module_id,
588 candidate.release.version,
589 ManagedDeliveryKind::from(&candidate.release.delivery),
590 candidate.release_digest
591 )
592 })
593 .collect();
594 error.conflict.eligible_alternatives.sort();
595 error
596}
597
598#[allow(clippy::too_many_lines)]
599fn build_lock(
600 request: &ModuleResolutionRequest,
601 desired: &DesiredModuleComposition,
602 desired_digest: &str,
603 candidates: &[ModuleResolutionCandidate],
604 solved: &SolverState,
605 direct_ids: &BTreeSet<String>,
606 selected_optional: &BTreeMap<String, BTreeSet<String>>,
607) -> ApplicationModuleLock {
608 let overrides = desired
609 .local_overrides
610 .iter()
611 .map(|item| (item.module_id.as_str(), item.content_digest.clone()))
612 .collect::<BTreeMap<_, _>>();
613 let mut modules = solved
614 .selections
615 .iter()
616 .map(|(module_id, index)| {
617 let candidate = &candidates[*index];
618 let release = &candidate.release;
619 let dependency_module_ids = release
620 .manifest
621 .requires
622 .iter()
623 .filter(|requirement| {
624 !requirement.optional
625 || selected_optional
626 .get(module_id)
627 .is_some_and(|selected| selected.contains(&requirement.module_id))
628 })
629 .map(|requirement| requirement.module_id.clone())
630 .collect::<Vec<_>>();
631 let (crate_features, migration_artifacts) = match &release.delivery {
632 ModuleDelivery::Linked(linked) => {
633 (linked.features.clone(), linked.migrations.clone())
634 }
635 ModuleDelivery::Service(_) => (Vec::new(), Vec::new()),
636 };
637 LockedModule {
638 module_id: module_id.clone(),
639 version: release.version.clone(),
640 release_digest: candidate.release_digest.clone(),
641 manifest_digest: release.manifest_digest.clone(),
642 delivery: release.delivery.clone(),
643 reason: if direct_ids.contains(module_id) {
644 LockedModuleReason::Direct
645 } else {
646 LockedModuleReason::Transitive
647 },
648 dependency_module_ids,
649 crate_features,
650 migration_artifacts,
651 console_ui_artifact: release.console_ui_artifact.as_ref().map(|artifact| {
652 LockedConsoleUiArtifact {
653 locator: artifact.artifact.locator.clone(),
654 digest: artifact.artifact.digest.clone(),
655 format: artifact.format.clone(),
656 protocol_major: artifact.protocol_major,
657 entry: artifact.entry.clone(),
658 entries: artifact.entries.clone(),
659 style_assets: artifact.style_assets.clone(),
660 manifest: artifact.manifest.clone(),
661 requested_permissions: artifact.requested_permissions.clone(),
662 }
663 }),
664 verification: candidate.eligibility.verification.clone(),
665 verification_cell: candidate.verification_cell.clone(),
666 lifecycle: candidate.eligibility.lifecycle.clone(),
667 local_override_digest: overrides.get(module_id.as_str()).cloned(),
668 }
669 })
670 .collect::<Vec<_>>();
671 modules.sort_by(|left, right| left.module_id.cmp(&right.module_id));
672
673 let mut capability_bindings = solved
674 .selections
675 .iter()
676 .flat_map(|(consumer, index)| {
677 candidates[*index]
678 .release
679 .manifest
680 .requires
681 .iter()
682 .filter(|requirement| {
683 !requirement.optional
684 || selected_optional
685 .get(consumer)
686 .is_some_and(|selected| selected.contains(&requirement.module_id))
687 })
688 .flat_map(move |requirement| {
689 requirement
690 .capabilities
691 .iter()
692 .map(move |capability| LockedCapabilityBinding {
693 capability: capability.clone(),
694 provider_module_id: requirement.module_id.clone(),
695 consumer_module_id: consumer.clone(),
696 })
697 })
698 })
699 .collect::<Vec<_>>();
700 capability_bindings.sort_by(|left, right| {
701 left.consumer_module_id
702 .cmp(&right.consumer_module_id)
703 .then_with(|| left.capability.cmp(&right.capability))
704 .then_with(|| left.provider_module_id.cmp(&right.provider_module_id))
705 });
706
707 ApplicationModuleLock {
708 protocol: APPLICATION_MODULE_LOCK_PROTOCOL.to_owned(),
709 application_id: desired.application_id.clone(),
710 desired_composition_digest: desired_digest.to_owned(),
711 catalog_snapshot_digest: request.catalog_snapshot_digest.clone(),
712 trust_policy_digest: request.trust_policy_digest.clone(),
713 resolver_version: request.resolver_version.clone(),
714 modules,
715 capability_bindings,
716 }
717}
718
719fn validate_optional_selections(
720 desired: &DesiredModuleComposition,
721 candidates: &[ModuleResolutionCandidate],
722 solved: &SolverState,
723) -> Result<(), ModuleResolutionError> {
724 for selection in &desired.selected {
725 let Some(candidate_index) = solved.selections.get(&selection.module_id) else {
726 continue;
727 };
728 let declared = candidates[*candidate_index]
729 .release
730 .manifest
731 .requires
732 .iter()
733 .filter(|requirement| requirement.optional)
734 .map(|requirement| requirement.module_id.as_str())
735 .collect::<BTreeSet<_>>();
736 if let Some(requirement) = selection
737 .optional_requirements
738 .iter()
739 .find(|requirement| !declared.contains(requirement.as_str()))
740 {
741 let mut error = conflict(
742 "optional_requirement_not_declared",
743 Some(&selection.module_id),
744 );
745 error.conflict.constraints.push(requirement.clone());
746 error
747 .conflict
748 .dependency_paths
749 .push(vec![selection.module_id.clone(), requirement.clone()]);
750 return Err(error);
751 }
752 }
753 Ok(())
754}
755
756fn conflict(code: &str, module_id: Option<&str>) -> ModuleResolutionError {
757 ModuleResolutionError {
758 conflict: Box::new(ModuleResolutionConflict {
759 protocol: MODULE_RESOLUTION_CONFLICT_PROTOCOL.to_owned(),
760 code: code.to_owned(),
761 module_id: module_id.map(str::to_owned),
762 dependency_paths: Vec::new(),
763 constraints: Vec::new(),
764 eligible_alternatives: Vec::new(),
765 }),
766 }
767}