1pub mod conflict_service;
20pub mod dependency_graph;
21pub mod lockfile_builder;
22pub mod path_resolver;
23pub mod pattern_expander;
24pub mod resource_service;
25pub mod source_context;
26pub mod transitive_resolver;
27pub mod types;
28pub mod version_resolver;
29
30pub use path_resolver::{extract_meaningful_path, is_file_relative_path, normalize_bare_filename};
32
33use std::collections::HashMap;
34use std::path::{Path, PathBuf};
35use std::sync::Arc;
36
37use anyhow::Result;
38
39use crate::cache::Cache;
40use crate::core::{OperationContext, ResourceType};
41use crate::lockfile::{LockFile, LockedResource};
42use crate::manifest::{Manifest, ResourceDependency};
43use crate::source::SourceManager;
44
45pub use conflict_service::ConflictService;
47pub use pattern_expander::PatternExpansionService;
48pub use resource_service::ResourceFetchingService;
49pub use types::ResolutionCore;
50pub use version_resolver::{
51 VersionResolutionService, VersionResolver as VersionResolverExport, find_best_matching_tag,
52 is_version_constraint, parse_tags_to_versions,
53};
54
55pub use dependency_graph::{DependencyGraph, DependencyNode};
57pub use lockfile_builder::LockfileBuilder;
58pub use pattern_expander::{expand_pattern_to_concrete_deps, generate_dependency_name};
59pub use types::{
60 DependencyKey, ManifestOverride, ManifestOverrideIndex, OverrideKey, ResolutionContext,
61 TransitiveContext,
62};
63
64pub use version_resolver::{PreparedSourceVersion, VersionResolver, WorktreeManager};
65
66#[allow(dead_code)] pub struct DependencyResolver {
73 core: ResolutionCore,
75
76 version_service: VersionResolutionService,
78
79 pattern_service: PatternExpansionService,
81
82 conflict_service: ConflictService,
84
85 resource_service: ResourceFetchingService,
87
88 conflict_detector: crate::version::conflict::ConflictDetector,
90
91 dependency_map: HashMap<DependencyKey, Vec<String>>,
93
94 pattern_alias_map: HashMap<(ResourceType, String), String>,
96
97 transitive_custom_names: HashMap<DependencyKey, String>,
99
100 sources_pre_synced: bool,
102}
103
104impl DependencyResolver {
105 pub async fn new(manifest: Manifest, cache: Cache) -> Result<Self> {
116 Self::new_with_context(manifest, cache, None).await
117 }
118
119 pub async fn new_with_context(
131 manifest: Manifest,
132 cache: Cache,
133 operation_context: Option<Arc<OperationContext>>,
134 ) -> Result<Self> {
135 let source_manager = SourceManager::from_manifest(&manifest)?;
137
138 let core = ResolutionCore::new(manifest, cache, source_manager, operation_context);
140
141 let version_service = VersionResolutionService::new(core.cache().clone());
143 let pattern_service = PatternExpansionService::new();
144 let conflict_service = ConflictService::new();
145 let resource_service = ResourceFetchingService::new();
146
147 Ok(Self {
148 core,
149 version_service,
150 pattern_service,
151 conflict_service,
152 resource_service,
153 conflict_detector: crate::version::conflict::ConflictDetector::new(),
154 dependency_map: HashMap::new(),
155 pattern_alias_map: HashMap::new(),
156 transitive_custom_names: HashMap::new(),
157 sources_pre_synced: false,
158 })
159 }
160
161 pub async fn new_with_global(manifest: Manifest, cache: Cache) -> Result<Self> {
174 Self::new_with_global_context(manifest, cache, None).await
175 }
176
177 pub async fn with_cache(manifest: Manifest, cache: Cache) -> Result<Self> {
187 Self::new_with_context(manifest, cache, None).await
188 }
189
190 pub async fn new_with_global_context(
204 manifest: Manifest,
205 cache: Cache,
206 _operation_context: Option<Arc<OperationContext>>,
207 ) -> Result<Self> {
208 let source_manager = SourceManager::from_manifest_with_global(&manifest).await?;
209
210 let core = ResolutionCore::new(manifest, cache, source_manager, _operation_context);
211
212 let version_service = VersionResolutionService::new(core.cache().clone());
213 let pattern_service = PatternExpansionService::new();
214 let conflict_service = ConflictService::new();
215 let resource_service = ResourceFetchingService::new();
216
217 Ok(Self {
218 core,
219 version_service,
220 pattern_service,
221 conflict_service,
222 resource_service,
223 conflict_detector: crate::version::conflict::ConflictDetector::new(),
224 dependency_map: HashMap::new(),
225 pattern_alias_map: HashMap::new(),
226 transitive_custom_names: HashMap::new(),
227 sources_pre_synced: false,
228 })
229 }
230
231 pub fn core(&self) -> &ResolutionCore {
233 &self.core
234 }
235
236 pub async fn resolve(&mut self) -> Result<LockFile> {
244 self.resolve_with_options(true).await
245 }
246
247 pub async fn resolve_with_options(&mut self, enable_transitive: bool) -> Result<LockFile> {
257 let mut lockfile = LockFile::new();
258
259 for (name, url) in &self.core.manifest().sources {
261 lockfile.add_source(name.clone(), url.clone(), String::new());
262 }
263
264 let base_deps: Vec<(String, ResourceDependency, ResourceType)> = self
266 .core
267 .manifest()
268 .all_dependencies_with_types()
269 .into_iter()
270 .map(|(name, dep, resource_type)| (name.to_string(), dep.into_owned(), resource_type))
271 .collect();
272
273 for (name, dep, _) in &base_deps {
275 self.add_to_conflict_detector(name, dep, "manifest");
276 }
277
278 if !self.sources_pre_synced {
280 let deps_for_sync: Vec<(String, ResourceDependency)> =
281 base_deps.iter().map(|(name, dep, _)| (name.clone(), dep.clone())).collect();
282 self.version_service.pre_sync_sources(&self.core, &deps_for_sync).await?;
283 self.sources_pre_synced = true;
284 }
285
286 let all_deps = if enable_transitive {
288 self.resolve_transitive_dependencies(&base_deps).await?
289 } else {
290 base_deps.clone()
291 };
292
293 for (name, dep, resource_type) in &all_deps {
295 if dep.is_pattern() {
296 let entries = self.resolve_pattern_dependency(name, dep, *resource_type).await?;
298
299 for entry in entries {
301 let entry_name = entry.name.clone();
302 self.add_or_update_lockfile_entry(&mut lockfile, &entry_name, entry);
303 }
304 } else {
305 let entry = self.resolve_dependency(name, dep, *resource_type).await?;
307 self.add_or_update_lockfile_entry(&mut lockfile, name, entry);
308 }
309 }
310
311 let conflicts = self.conflict_detector.detect_conflicts();
313 if !conflicts.is_empty() {
314 let mut error_msg = String::from("Version conflicts detected:\n\n");
315 for conflict in &conflicts {
316 error_msg.push_str(&format!("{conflict}\n"));
317 }
318 return Err(anyhow::anyhow!("{}", error_msg));
319 }
320
321 self.add_version_to_dependencies(&mut lockfile)?;
323 self.detect_target_conflicts(&lockfile)?;
324
325 Ok(lockfile)
326 }
327
328 pub async fn pre_sync_sources(&mut self, deps: &[(String, ResourceDependency)]) -> Result<()> {
341 self.version_service.pre_sync_sources(&self.core, deps).await?;
343 self.sources_pre_synced = true;
344 Ok(())
345 }
346
347 pub async fn update(
358 &mut self,
359 existing: &LockFile,
360 deps_to_update: Option<Vec<String>>,
361 ) -> Result<LockFile> {
362 let _existing = existing; let _deps_to_update = deps_to_update; self.resolve_with_options(true).await
367 }
368
369 pub async fn get_available_versions(&self, repo_path: &Path) -> Result<Vec<String>> {
379 VersionResolutionService::get_available_versions(&self.core, repo_path).await
380 }
381
382 pub async fn verify(&self, _lockfile: &LockFile) -> Result<()> {
392 Ok(())
394 }
395
396 pub fn operation_context(&self) -> Option<&Arc<OperationContext>> {
398 self.core.operation_context()
399 }
400
401 pub fn set_operation_context(&mut self, context: Arc<OperationContext>) {
407 self.core.operation_context = Some(context);
408 }
409}
410
411impl DependencyResolver {
413 fn build_manifest_override_index(
420 &self,
421 base_deps: &[(String, ResourceDependency, ResourceType)],
422 ) -> types::ManifestOverrideIndex {
423 use crate::resolver::types::{ManifestOverride, OverrideKey, normalize_lookup_path};
424
425 let mut index = HashMap::new();
426
427 for (name, dep, resource_type) in base_deps {
428 if dep.is_pattern() {
430 continue;
431 }
432
433 let normalized_path = normalize_lookup_path(dep.get_path());
435 let source = dep.get_source().map(std::string::ToString::to_string);
436
437 let tool = dep
439 .get_tool()
440 .map(str::to_string)
441 .unwrap_or_else(|| self.core.manifest().get_default_tool(*resource_type));
442
443 let merged_variant_inputs =
446 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep);
447 let variant_hash = crate::utils::compute_variant_inputs_hash(&merged_variant_inputs)
448 .unwrap_or_else(|_| crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string());
449
450 let key = OverrideKey {
451 resource_type: *resource_type,
452 normalized_path,
453 source,
454 tool,
455 variant_hash,
456 };
457
458 let override_info = ManifestOverride {
460 filename: dep.get_filename().map(std::string::ToString::to_string),
461 target: dep.get_target().map(std::string::ToString::to_string),
462 install: dep.get_install(),
463 manifest_alias: Some(name.clone()),
464 template_vars: dep.get_template_vars().cloned(),
465 };
466
467 tracing::debug!(
468 "Adding manifest override for {:?}:{} (tool={}, variant_hash={})",
469 resource_type,
470 dep.get_path(),
471 key.tool,
472 key.variant_hash
473 );
474
475 index.insert(key, override_info);
476 }
477
478 tracing::info!("Built manifest override index with {} entries", index.len());
479 index
480 }
481
482 async fn resolve_transitive_dependencies(
488 &mut self,
489 base_deps: &[(String, ResourceDependency, ResourceType)],
490 ) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
491 use crate::resolver::transitive_resolver;
492
493 let manifest_overrides = self.build_manifest_override_index(base_deps);
495
496 let resolution_ctx = ResolutionContext {
498 manifest: self.core.manifest(),
499 cache: self.core.cache(),
500 source_manager: self.core.source_manager(),
501 operation_context: self.core.operation_context(),
502 };
503
504 let mut ctx = TransitiveContext {
506 base: resolution_ctx,
507 dependency_map: &mut self.dependency_map,
508 transitive_custom_names: &mut self.transitive_custom_names,
509 conflict_detector: &mut self.conflict_detector,
510 manifest_overrides: &manifest_overrides,
511 };
512
513 let prepared_versions = self.version_service.prepared_versions().clone();
515
516 let mut services = transitive_resolver::ResolutionServices {
518 version_service: &mut self.version_service,
519 pattern_service: &mut self.pattern_service,
520 };
521
522 transitive_resolver::resolve_with_services(
524 &mut ctx,
525 &self.core,
526 base_deps,
527 true, &prepared_versions,
529 &mut self.pattern_alias_map,
530 &mut services,
531 )
532 .await
533 }
534
535 fn get_dependencies_for(
540 &self,
541 name: &str,
542 source: Option<&str>,
543 resource_type: ResourceType,
544 tool: Option<&str>,
545 variant_hash: &str,
546 ) -> Vec<String> {
547 let key = (
548 resource_type,
549 name.to_string(),
550 source.map(std::string::ToString::to_string),
551 tool.map(std::string::ToString::to_string),
552 variant_hash.to_string(),
553 );
554 let result = self.dependency_map.get(&key).cloned().unwrap_or_default();
555 tracing::debug!(
556 "[DEBUG] get_dependencies_for: name='{}', type={:?}, source={:?}, tool={:?}, hash={}, found={} deps",
557 name,
558 resource_type,
559 source,
560 tool,
561 &variant_hash[..8],
562 result.len()
563 );
564 result
565 }
566
567 fn get_pattern_alias_for_dependency(
571 &self,
572 name: &str,
573 resource_type: ResourceType,
574 ) -> Option<String> {
575 self.pattern_alias_map.get(&(resource_type, name.to_string())).cloned()
577 }
578
579 async fn resolve_dependency(
584 &mut self,
585 name: &str,
586 dep: &ResourceDependency,
587 resource_type: ResourceType,
588 ) -> Result<LockedResource> {
589 use crate::resolver::lockfile_builder;
590 use crate::resolver::path_resolver as install_path_resolver;
591 use crate::utils::normalize_path_for_storage;
592
593 tracing::debug!(
594 "resolve_dependency: name={}, path={}, source={:?}, is_local={}",
595 name,
596 dep.get_path(),
597 dep.get_source(),
598 dep.is_local()
599 );
600
601 if dep.is_local() {
602 let filename = if let Some(custom_filename) = dep.get_filename() {
604 custom_filename.to_string()
605 } else {
606 extract_meaningful_path(Path::new(dep.get_path()))
607 };
608
609 let artifact_type_string = dep
610 .get_tool()
611 .map(|s| s.to_string())
612 .unwrap_or_else(|| self.core.manifest().get_default_tool(resource_type));
613 let artifact_type = artifact_type_string.as_str();
614
615 let installed_at = install_path_resolver::resolve_install_path(
616 self.core.manifest(),
617 dep,
618 artifact_type,
619 resource_type,
620 &filename,
621 )?;
622
623 let has_pattern_alias = self.get_pattern_alias_for_dependency(name, resource_type);
625 let is_in_manifest = self
626 .core
627 .manifest()
628 .get_dependencies(resource_type)
629 .is_some_and(|deps| deps.contains_key(name));
630
631 let manifest_alias = if let Some(ref pattern_alias) = has_pattern_alias {
632 Some(pattern_alias.clone())
634 } else if is_in_manifest {
635 Some(name.to_string())
637 } else {
638 None
640 };
641
642 tracing::debug!(
643 "manifest_alias calculation: name={}, path={}, has_pattern_alias={}, is_in_manifest={}, manifest_alias={:?}",
644 name,
645 dep.get_path(),
646 has_pattern_alias.is_some(),
647 is_in_manifest,
648 manifest_alias
649 );
650
651 let applied_patches = lockfile_builder::get_patches_for_resource(
652 self.core.manifest(),
653 resource_type,
654 name,
655 manifest_alias.as_deref(),
656 );
657
658 let canonical_name =
660 if let Some(manifest_dir) = self.core.manifest().manifest_dir.as_ref() {
661 let full_path = if Path::new(dep.get_path()).is_absolute() {
663 PathBuf::from(dep.get_path())
664 } else {
665 manifest_dir.join(dep.get_path())
666 };
667
668 let canonical_path = crate::utils::fs::normalize_path(&full_path);
672
673 let source_context =
674 crate::resolver::source_context::SourceContext::local(manifest_dir);
675 generate_dependency_name(&canonical_path.to_string_lossy(), &source_context)
676 } else {
677 name.to_string()
679 };
680
681 let variant_inputs = lockfile_builder::VariantInputs::new(
682 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
683 );
684
685 Ok(LockedResource {
686 name: canonical_name,
687 source: None,
688 url: None,
689 path: normalize_path_for_storage(dep.get_path()),
690 version: None,
691 resolved_commit: None,
692 checksum: String::new(),
693 installed_at,
694 dependencies: self.get_dependencies_for(
695 name,
696 None,
697 resource_type,
698 Some(&artifact_type_string),
699 variant_inputs.hash(),
700 ),
701 resource_type,
702 tool: Some(artifact_type_string),
703 manifest_alias,
704 applied_patches,
705 install: dep.get_install(),
706 variant_inputs,
707 context_checksum: None,
708 })
709 } else {
710 let source_name = dep
712 .get_source()
713 .ok_or_else(|| anyhow::anyhow!("Dependency '{}' has no source specified", name))?;
714
715 let source_context =
717 crate::resolver::source_context::SourceContext::remote(source_name);
718 let canonical_name = generate_dependency_name(dep.get_path(), &source_context);
719
720 let source_url = self
721 .core
722 .source_manager()
723 .get_source_url(source_name)
724 .ok_or_else(|| anyhow::anyhow!("Source '{}' not found", source_name))?;
725
726 let version_key =
727 dep.get_version().map_or_else(|| "HEAD".to_string(), |v| v.to_string());
728 let group_key = format!("{}::{}", source_name, version_key);
729
730 let prepared =
731 self.version_service.get_prepared_version(&group_key).ok_or_else(|| {
732 anyhow::anyhow!(
733 "Prepared state missing for source '{}' @ '{}'",
734 source_name,
735 version_key
736 )
737 })?;
738
739 let filename = if let Some(custom_filename) = dep.get_filename() {
740 custom_filename.to_string()
741 } else {
742 Path::new(dep.get_path()).to_string_lossy().to_string()
743 };
744
745 let artifact_type_string = dep
746 .get_tool()
747 .map(|s| s.to_string())
748 .unwrap_or_else(|| self.core.manifest().get_default_tool(resource_type));
749 let artifact_type = artifact_type_string.as_str();
750
751 let installed_at = install_path_resolver::resolve_install_path(
752 self.core.manifest(),
753 dep,
754 artifact_type,
755 resource_type,
756 &filename,
757 )?;
758
759 let manifest_alias = if let Some(pattern_alias) =
761 self.get_pattern_alias_for_dependency(name, resource_type)
762 {
763 Some(pattern_alias)
765 } else if self
766 .core
767 .manifest()
768 .get_dependencies(resource_type)
769 .is_some_and(|deps| deps.contains_key(name))
770 {
771 Some(name.to_string())
773 } else {
774 None
776 };
777
778 let applied_patches = lockfile_builder::get_patches_for_resource(
779 self.core.manifest(),
780 resource_type,
781 name,
782 manifest_alias.as_deref(),
783 );
784
785 let variant_inputs = lockfile_builder::VariantInputs::new(
786 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
787 );
788
789 Ok(LockedResource {
790 name: canonical_name, source: Some(source_name.to_string()),
792 url: Some(source_url.clone()),
793 path: normalize_path_for_storage(dep.get_path()),
794 version: prepared.resolved_version.clone(),
795 resolved_commit: Some(prepared.resolved_commit.clone()),
796 checksum: String::new(),
797 installed_at,
798 dependencies: self.get_dependencies_for(
799 name,
800 Some(source_name),
801 resource_type,
802 Some(&artifact_type_string),
803 variant_inputs.hash(),
804 ),
805 resource_type,
806 tool: Some(artifact_type_string),
807 manifest_alias,
808 applied_patches,
809 install: dep.get_install(),
810 variant_inputs,
811 context_checksum: None,
812 })
813 }
814 }
815
816 async fn resolve_pattern_dependency(
818 &mut self,
819 name: &str,
820 dep: &ResourceDependency,
821 resource_type: ResourceType,
822 ) -> Result<Vec<LockedResource>> {
823 use crate::pattern::PatternResolver;
824 use crate::resolver::{
825 lockfile_builder, path_resolver as install_path_resolver, path_resolver,
826 };
827 use crate::utils::{
828 compute_relative_install_path, normalize_path, normalize_path_for_storage,
829 };
830
831 if !dep.is_pattern() {
832 return Err(anyhow::anyhow!(
833 "Expected pattern dependency but no glob characters found in path"
834 ));
835 }
836
837 let pattern = dep.get_path();
838
839 if dep.is_local() {
840 let (base_path, pattern_str) = path_resolver::parse_pattern_base_path(pattern);
842 let pattern_resolver = PatternResolver::new();
843 let matches = pattern_resolver.resolve(&pattern_str, &base_path)?;
844
845 let artifact_type_string = dep
846 .get_tool()
847 .map(|s| s.to_string())
848 .unwrap_or_else(|| self.core.manifest().get_default_tool(resource_type));
849 let artifact_type = artifact_type_string.as_str();
850
851 let variant_inputs = lockfile_builder::VariantInputs::new(
853 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
854 );
855
856 let mut resources = Vec::new();
857 for matched_path in matches {
858 let resource_name = crate::pattern::extract_resource_name(&matched_path);
859 let full_relative_path =
860 path_resolver::construct_full_relative_path(&base_path, &matched_path);
861 let filename = path_resolver::extract_pattern_filename(&base_path, &matched_path);
862
863 let installed_at = install_path_resolver::resolve_install_path(
864 self.core.manifest(),
865 dep,
866 artifact_type,
867 resource_type,
868 &filename,
869 )?;
870
871 resources.push(LockedResource {
872 name: resource_name.clone(),
873 source: None,
874 url: None,
875 path: full_relative_path,
876 version: None,
877 resolved_commit: None,
878 checksum: String::new(),
879 installed_at,
880 dependencies: vec![],
881 resource_type,
882 tool: Some(artifact_type_string.clone()),
883 manifest_alias: Some(name.to_string()),
884 applied_patches: lockfile_builder::get_patches_for_resource(
885 self.core.manifest(),
886 resource_type,
887 &resource_name, Some(name), ),
890 install: dep.get_install(),
891 variant_inputs: variant_inputs.clone(),
892 context_checksum: None,
893 });
894 }
895
896 Ok(resources)
897 } else {
898 let pattern_name = name;
901
902 let source_name = dep.get_source().ok_or_else(|| {
903 anyhow::anyhow!("Pattern dependency '{}' has no source specified", name)
904 })?;
905
906 let source_url = self
907 .core
908 .source_manager()
909 .get_source_url(source_name)
910 .ok_or_else(|| anyhow::anyhow!("Source '{}' not found", source_name))?;
911
912 let version_key =
913 dep.get_version().map_or_else(|| "HEAD".to_string(), |v| v.to_string());
914 let group_key = format!("{}::{}", source_name, version_key);
915
916 let prepared =
917 self.version_service.get_prepared_version(&group_key).ok_or_else(|| {
918 anyhow::anyhow!(
919 "Prepared state missing for source '{}' @ '{}'",
920 source_name,
921 version_key
922 )
923 })?;
924
925 let repo_path = Path::new(&prepared.worktree_path);
926 let pattern_resolver = PatternResolver::new();
927 let matches = pattern_resolver.resolve(pattern, repo_path)?;
928
929 let artifact_type_string = dep
930 .get_tool()
931 .map(|s| s.to_string())
932 .unwrap_or_else(|| self.core.manifest().get_default_tool(resource_type));
933 let artifact_type = artifact_type_string.as_str();
934
935 let variant_inputs = lockfile_builder::VariantInputs::new(
937 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
938 );
939
940 let mut resources = Vec::new();
941 for matched_path in matches {
942 let resource_name = crate::pattern::extract_resource_name(&matched_path);
943
944 let installed_at = match resource_type {
946 ResourceType::Hook | ResourceType::McpServer => {
947 install_path_resolver::resolve_merge_target_path(
948 self.core.manifest(),
949 artifact_type,
950 resource_type,
951 )
952 }
953 _ => {
954 let artifact_path = self
955 .core
956 .manifest()
957 .get_artifact_resource_path(artifact_type, resource_type)
958 .ok_or_else(|| {
959 anyhow::anyhow!(
960 "Resource type '{}' is not supported by tool '{}'",
961 resource_type,
962 artifact_type
963 )
964 })?;
965
966 let dep_flatten = dep.get_flatten();
967 let tool_flatten = self
968 .core
969 .manifest()
970 .get_tool_config(artifact_type)
971 .and_then(|config| config.resources.get(resource_type.to_plural()))
972 .and_then(|resource_config| resource_config.flatten);
973
974 let flatten = dep_flatten.or(tool_flatten).unwrap_or(false);
975
976 let base_target = if let Some(custom_target) = dep.get_target() {
977 PathBuf::from(artifact_path.display().to_string())
978 .join(custom_target.trim_start_matches('/'))
979 } else {
980 artifact_path.to_path_buf()
981 };
982
983 let filename = repo_path.join(&matched_path).to_string_lossy().to_string();
984 let relative_path = compute_relative_install_path(
985 &base_target,
986 Path::new(&filename),
987 flatten,
988 );
989 normalize_path_for_storage(normalize_path(&base_target.join(relative_path)))
990 }
991 };
992
993 resources.push(LockedResource {
994 name: resource_name.clone(),
995 source: Some(source_name.to_string()),
996 url: Some(source_url.clone()),
997 path: normalize_path_for_storage(matched_path.to_string_lossy().to_string()),
998 version: prepared.resolved_version.clone(),
999 resolved_commit: Some(prepared.resolved_commit.clone()),
1000 checksum: String::new(),
1001 installed_at,
1002 dependencies: vec![],
1003 resource_type,
1004 tool: Some(artifact_type_string.clone()),
1005 manifest_alias: Some(pattern_name.to_string()),
1006 applied_patches: lockfile_builder::get_patches_for_resource(
1007 self.core.manifest(),
1008 resource_type,
1009 &resource_name, Some(pattern_name), ),
1012 install: dep.get_install(),
1013 variant_inputs: variant_inputs.clone(),
1014 context_checksum: None,
1015 });
1016 }
1017
1018 Ok(resources)
1019 }
1020 }
1021
1022 fn add_or_update_lockfile_entry(
1024 &self,
1025 lockfile: &mut LockFile,
1026 _name: &str,
1027 entry: LockedResource,
1028 ) {
1029 let resources = lockfile.get_resources_mut(&entry.resource_type);
1030
1031 if let Some(existing) =
1032 resources.iter_mut().find(|e| lockfile_builder::is_duplicate_entry(e, &entry))
1033 {
1034 let existing_is_direct = existing.manifest_alias.is_some();
1037 let new_is_direct = entry.manifest_alias.is_some();
1038
1039 if new_is_direct || !existing_is_direct {
1040 tracing::debug!(
1044 "Replacing {} (direct={}) with {} (direct={})",
1045 existing.name,
1046 existing_is_direct,
1047 entry.name,
1048 new_is_direct
1049 );
1050 *existing = entry;
1051 } else {
1052 tracing::debug!("Keeping direct {} over transitive {}", existing.name, entry.name);
1054 }
1055 } else {
1056 resources.push(entry);
1057 }
1058 }
1059
1060 fn add_version_to_dependencies(&self, lockfile: &mut LockFile) -> Result<()> {
1062 use crate::resolver::lockfile_builder;
1063
1064 lockfile_builder::add_version_to_all_dependencies(lockfile);
1065 Ok(())
1066 }
1067
1068 fn detect_target_conflicts(&self, lockfile: &LockFile) -> Result<()> {
1070 use crate::resolver::lockfile_builder;
1071
1072 lockfile_builder::detect_target_conflicts(lockfile)
1073 }
1074
1075 fn add_to_conflict_detector(
1077 &mut self,
1078 _name: &str,
1079 dep: &ResourceDependency,
1080 required_by: &str,
1081 ) {
1082 use crate::resolver::types as dependency_helpers;
1083
1084 if dep.is_local() {
1086 return;
1087 }
1088
1089 let resource_id = dependency_helpers::build_resource_id(dep);
1091
1092 let version = dep.get_version().unwrap_or("HEAD");
1094
1095 self.conflict_detector.add_requirement(&resource_id, required_by, version);
1097 }
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102 use super::*;
1103
1104 #[tokio::test]
1105 async fn test_resolver_creation() {
1106 let manifest = Manifest::default();
1107 let cache = Cache::new().unwrap();
1108 let resolver = DependencyResolver::new(manifest, cache).await;
1109 assert!(resolver.is_ok());
1110 }
1111
1112 #[tokio::test]
1113 async fn test_resolver_with_global() {
1114 let manifest = Manifest::default();
1115 let cache = Cache::new().unwrap();
1116 let resolver = DependencyResolver::new_with_global(manifest, cache).await;
1117 assert!(resolver.is_ok());
1118 }
1119}