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
66pub struct DependencyResolver {
72 core: ResolutionCore,
74
75 version_service: VersionResolutionService,
77
78 pattern_service: PatternExpansionService,
80
81 conflict_detector: crate::version::conflict::ConflictDetector,
83
84 dependency_map: HashMap<DependencyKey, Vec<String>>,
86
87 pattern_alias_map: HashMap<(ResourceType, String), String>,
89
90 transitive_custom_names: HashMap<DependencyKey, String>,
92
93 sources_pre_synced: bool,
95}
96
97impl DependencyResolver {
98 pub async fn new(manifest: Manifest, cache: Cache) -> Result<Self> {
109 Self::new_with_context(manifest, cache, None).await
110 }
111
112 pub async fn new_with_context(
124 manifest: Manifest,
125 cache: Cache,
126 operation_context: Option<Arc<OperationContext>>,
127 ) -> Result<Self> {
128 let source_manager = SourceManager::from_manifest(&manifest)?;
130
131 let core = ResolutionCore::new(manifest, cache, source_manager, operation_context);
133
134 let version_service = VersionResolutionService::new(core.cache().clone());
136 let pattern_service = PatternExpansionService::new();
137
138 Ok(Self {
139 core,
140 version_service,
141 pattern_service,
142 conflict_detector: crate::version::conflict::ConflictDetector::new(),
143 dependency_map: HashMap::new(),
144 pattern_alias_map: HashMap::new(),
145 transitive_custom_names: HashMap::new(),
146 sources_pre_synced: false,
147 })
148 }
149
150 pub async fn new_with_global(manifest: Manifest, cache: Cache) -> Result<Self> {
163 Self::new_with_global_context(manifest, cache, None).await
164 }
165
166 pub async fn with_cache(manifest: Manifest, cache: Cache) -> Result<Self> {
176 Self::new_with_context(manifest, cache, None).await
177 }
178
179 pub async fn new_with_global_context(
193 manifest: Manifest,
194 cache: Cache,
195 _operation_context: Option<Arc<OperationContext>>,
196 ) -> Result<Self> {
197 let source_manager = SourceManager::from_manifest_with_global(&manifest).await?;
198
199 let core = ResolutionCore::new(manifest, cache, source_manager, _operation_context);
200
201 let version_service = VersionResolutionService::new(core.cache().clone());
202 let pattern_service = PatternExpansionService::new();
203
204 Ok(Self {
205 core,
206 version_service,
207 pattern_service,
208 conflict_detector: crate::version::conflict::ConflictDetector::new(),
209 dependency_map: HashMap::new(),
210 pattern_alias_map: HashMap::new(),
211 transitive_custom_names: HashMap::new(),
212 sources_pre_synced: false,
213 })
214 }
215
216 pub fn core(&self) -> &ResolutionCore {
218 &self.core
219 }
220
221 pub async fn resolve(&mut self) -> Result<LockFile> {
229 self.resolve_with_options(true).await
230 }
231
232 pub async fn resolve_with_options(&mut self, enable_transitive: bool) -> Result<LockFile> {
242 let mut lockfile = LockFile::new();
243
244 for (name, url) in &self.core.manifest().sources {
246 lockfile.add_source(name.clone(), url.clone(), String::new());
247 }
248
249 let base_deps: Vec<(String, ResourceDependency, ResourceType)> = self
251 .core
252 .manifest()
253 .all_dependencies_with_types()
254 .into_iter()
255 .map(|(name, dep, resource_type)| (name.to_string(), dep.into_owned(), resource_type))
256 .collect();
257
258 for (name, dep, _) in &base_deps {
260 self.add_to_conflict_detector(name, dep, "manifest");
261 }
262
263 if !self.sources_pre_synced {
265 let deps_for_sync: Vec<(String, ResourceDependency)> =
266 base_deps.iter().map(|(name, dep, _)| (name.clone(), dep.clone())).collect();
267 self.version_service.pre_sync_sources(&self.core, &deps_for_sync).await?;
268 self.sources_pre_synced = true;
269 }
270
271 let all_deps = if enable_transitive {
273 self.resolve_transitive_dependencies(&base_deps).await?
274 } else {
275 base_deps.clone()
276 };
277
278 for (name, dep, resource_type) in &all_deps {
280 if dep.is_pattern() {
281 let entries = self.resolve_pattern_dependency(name, dep, *resource_type).await?;
283
284 for entry in entries {
286 let entry_name = entry.name.clone();
287 self.add_or_update_lockfile_entry(&mut lockfile, &entry_name, entry);
288 }
289 } else {
290 let entry = self.resolve_dependency(name, dep, *resource_type).await?;
292 self.add_or_update_lockfile_entry(&mut lockfile, name, entry);
293 }
294 }
295
296 let conflicts = self.conflict_detector.detect_conflicts();
298 if !conflicts.is_empty() {
299 let mut error_msg = String::from("Version conflicts detected:\n\n");
300 for conflict in &conflicts {
301 error_msg.push_str(&format!("{conflict}\n"));
302 }
303 return Err(anyhow::anyhow!("{}", error_msg));
304 }
305
306 self.add_version_to_dependencies(&mut lockfile)?;
308 self.detect_target_conflicts(&lockfile)?;
309
310 Ok(lockfile)
311 }
312
313 pub async fn pre_sync_sources(&mut self, deps: &[(String, ResourceDependency)]) -> Result<()> {
326 self.version_service.pre_sync_sources(&self.core, deps).await?;
328 self.sources_pre_synced = true;
329 Ok(())
330 }
331
332 pub async fn update(
343 &mut self,
344 existing: &LockFile,
345 deps_to_update: Option<Vec<String>>,
346 ) -> Result<LockFile> {
347 let _existing = existing; let _deps_to_update = deps_to_update; self.resolve_with_options(true).await
352 }
353
354 pub async fn get_available_versions(&self, repo_path: &Path) -> Result<Vec<String>> {
364 VersionResolutionService::get_available_versions(&self.core, repo_path).await
365 }
366
367 pub async fn verify(&self, _lockfile: &LockFile) -> Result<()> {
377 Ok(())
379 }
380
381 pub fn operation_context(&self) -> Option<&Arc<OperationContext>> {
383 self.core.operation_context()
384 }
385
386 pub fn set_operation_context(&mut self, context: Arc<OperationContext>) {
392 self.core.operation_context = Some(context);
393 }
394}
395
396impl DependencyResolver {
398 fn build_manifest_override_index(
405 &self,
406 base_deps: &[(String, ResourceDependency, ResourceType)],
407 ) -> types::ManifestOverrideIndex {
408 use crate::resolver::types::{ManifestOverride, OverrideKey, normalize_lookup_path};
409
410 let mut index = HashMap::new();
411
412 for (name, dep, resource_type) in base_deps {
413 if dep.is_pattern() {
415 continue;
416 }
417
418 let normalized_path = normalize_lookup_path(dep.get_path());
420 let source = dep.get_source().map(std::string::ToString::to_string);
421
422 let tool = dep
424 .get_tool()
425 .map(str::to_string)
426 .unwrap_or_else(|| self.core.manifest().get_default_tool(*resource_type));
427
428 let merged_variant_inputs =
431 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep);
432 let variant_hash = crate::utils::compute_variant_inputs_hash(&merged_variant_inputs)
433 .unwrap_or_else(|_| crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string());
434
435 let key = OverrideKey {
436 resource_type: *resource_type,
437 normalized_path,
438 source,
439 tool,
440 variant_hash,
441 };
442
443 let override_info = ManifestOverride {
445 filename: dep.get_filename().map(std::string::ToString::to_string),
446 target: dep.get_target().map(std::string::ToString::to_string),
447 install: dep.get_install(),
448 manifest_alias: Some(name.clone()),
449 template_vars: dep.get_template_vars().cloned(),
450 };
451
452 tracing::debug!(
453 "Adding manifest override for {:?}:{} (tool={}, variant_hash={})",
454 resource_type,
455 dep.get_path(),
456 key.tool,
457 key.variant_hash
458 );
459
460 index.insert(key, override_info);
461 }
462
463 tracing::info!("Built manifest override index with {} entries", index.len());
464 index
465 }
466
467 async fn resolve_transitive_dependencies(
473 &mut self,
474 base_deps: &[(String, ResourceDependency, ResourceType)],
475 ) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
476 use crate::resolver::transitive_resolver;
477
478 let manifest_overrides = self.build_manifest_override_index(base_deps);
480
481 let resolution_ctx = ResolutionContext {
483 manifest: self.core.manifest(),
484 cache: self.core.cache(),
485 source_manager: self.core.source_manager(),
486 operation_context: self.core.operation_context(),
487 };
488
489 let mut ctx = TransitiveContext {
491 base: resolution_ctx,
492 dependency_map: &mut self.dependency_map,
493 transitive_custom_names: &mut self.transitive_custom_names,
494 conflict_detector: &mut self.conflict_detector,
495 manifest_overrides: &manifest_overrides,
496 };
497
498 let prepared_versions = self.version_service.prepared_versions().clone();
500
501 let mut services = transitive_resolver::ResolutionServices {
503 version_service: &mut self.version_service,
504 pattern_service: &mut self.pattern_service,
505 };
506
507 transitive_resolver::resolve_with_services(
509 &mut ctx,
510 &self.core,
511 base_deps,
512 true, &prepared_versions,
514 &mut self.pattern_alias_map,
515 &mut services,
516 )
517 .await
518 }
519
520 fn get_dependencies_for(
525 &self,
526 name: &str,
527 source: Option<&str>,
528 resource_type: ResourceType,
529 tool: Option<&str>,
530 variant_hash: &str,
531 ) -> Vec<String> {
532 let key = (
533 resource_type,
534 name.to_string(),
535 source.map(std::string::ToString::to_string),
536 tool.map(std::string::ToString::to_string),
537 variant_hash.to_string(),
538 );
539 let result = self.dependency_map.get(&key).cloned().unwrap_or_default();
540 tracing::debug!(
541 "[DEBUG] get_dependencies_for: name='{}', type={:?}, source={:?}, tool={:?}, hash={}, found={} deps",
542 name,
543 resource_type,
544 source,
545 tool,
546 &variant_hash[..8],
547 result.len()
548 );
549 result
550 }
551
552 fn get_pattern_alias_for_dependency(
556 &self,
557 name: &str,
558 resource_type: ResourceType,
559 ) -> Option<String> {
560 self.pattern_alias_map.get(&(resource_type, name.to_string())).cloned()
562 }
563
564 async fn resolve_dependency(
568 &mut self,
569 name: &str,
570 dep: &ResourceDependency,
571 resource_type: ResourceType,
572 ) -> Result<LockedResource> {
573 tracing::debug!(
574 "resolve_dependency: name={}, path={}, source={:?}, is_local={}",
575 name,
576 dep.get_path(),
577 dep.get_source(),
578 dep.is_local()
579 );
580
581 if dep.is_local() {
582 self.resolve_local_dependency(name, dep, resource_type)
583 } else {
584 self.resolve_git_dependency(name, dep, resource_type).await
585 }
586 }
587
588 fn resolve_filename(dep: &ResourceDependency) -> String {
593 dep.get_filename()
594 .map_or_else(|| extract_meaningful_path(Path::new(dep.get_path())), |f| f.to_string())
595 }
596
597 fn resolve_tool(&self, dep: &ResourceDependency, resource_type: ResourceType) -> String {
601 dep.get_tool()
602 .map(|s| s.to_string())
603 .unwrap_or_else(|| self.core.manifest().get_default_tool(resource_type))
604 }
605
606 fn resolve_manifest_alias(&self, name: &str, resource_type: ResourceType) -> Option<String> {
611 let has_pattern_alias = self.get_pattern_alias_for_dependency(name, resource_type);
612 let is_in_manifest = self
613 .core
614 .manifest()
615 .get_dependencies(resource_type)
616 .is_some_and(|deps| deps.contains_key(name));
617
618 if let Some(pattern_alias) = has_pattern_alias {
619 Some(pattern_alias)
621 } else if is_in_manifest {
622 Some(name.to_string())
624 } else {
625 None
627 }
628 }
629
630 fn resolve_local_dependency(
632 &self,
633 name: &str,
634 dep: &ResourceDependency,
635 resource_type: ResourceType,
636 ) -> Result<LockedResource> {
637 use crate::resolver::lockfile_builder;
638 use crate::resolver::path_resolver as install_path_resolver;
639 use crate::utils::normalize_path_for_storage;
640
641 let filename = Self::resolve_filename(dep);
642 let artifact_type_string = self.resolve_tool(dep, resource_type);
643 let artifact_type = artifact_type_string.as_str();
644
645 let installed_at = install_path_resolver::resolve_install_path(
646 self.core.manifest(),
647 dep,
648 artifact_type,
649 resource_type,
650 &filename,
651 )?;
652
653 let manifest_alias = self.resolve_manifest_alias(name, resource_type);
654
655 tracing::debug!(
656 "Local dependency: name={}, path={}, manifest_alias={:?}",
657 name,
658 dep.get_path(),
659 manifest_alias
660 );
661
662 let applied_patches = lockfile_builder::get_patches_for_resource(
663 self.core.manifest(),
664 resource_type,
665 name,
666 manifest_alias.as_deref(),
667 );
668
669 let canonical_name = self.compute_local_canonical_name(name, dep, &manifest_alias)?;
674
675 let variant_inputs = lockfile_builder::VariantInputs::new(
676 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
677 );
678
679 Ok(LockedResource {
680 name: canonical_name,
681 source: None,
682 url: None,
683 path: normalize_path_for_storage(dep.get_path()),
684 version: None,
685 resolved_commit: None,
686 checksum: String::new(),
687 installed_at,
688 dependencies: self.get_dependencies_for(
689 name,
690 None,
691 resource_type,
692 Some(&artifact_type_string),
693 variant_inputs.hash(),
694 ),
695 resource_type,
696 tool: Some(artifact_type_string),
697 manifest_alias,
698 applied_patches,
699 install: dep.get_install(),
700 variant_inputs,
701 context_checksum: None,
702 })
703 }
704
705 fn compute_local_canonical_name(
710 &self,
711 name: &str,
712 dep: &ResourceDependency,
713 manifest_alias: &Option<String>,
714 ) -> Result<String> {
715 if manifest_alias.is_none() {
716 Ok(name.to_string())
718 } else if let Some(manifest_dir) = self.core.manifest().manifest_dir.as_ref() {
719 let full_path = if Path::new(dep.get_path()).is_absolute() {
721 PathBuf::from(dep.get_path())
722 } else {
723 manifest_dir.join(dep.get_path())
724 };
725
726 let canonical_path = crate::utils::fs::normalize_path(&full_path);
728
729 let source_context =
730 crate::resolver::source_context::SourceContext::local(manifest_dir);
731 Ok(generate_dependency_name(&canonical_path.to_string_lossy(), &source_context))
732 } else {
733 Ok(name.to_string())
735 }
736 }
737
738 async fn resolve_git_dependency(
740 &mut self,
741 name: &str,
742 dep: &ResourceDependency,
743 resource_type: ResourceType,
744 ) -> Result<LockedResource> {
745 use crate::resolver::lockfile_builder;
746 use crate::resolver::path_resolver as install_path_resolver;
747 use crate::utils::normalize_path_for_storage;
748
749 let source_name = dep
750 .get_source()
751 .ok_or_else(|| anyhow::anyhow!("Dependency '{}' has no source specified", name))?;
752
753 let source_context = crate::resolver::source_context::SourceContext::remote(source_name);
755 let canonical_name = generate_dependency_name(dep.get_path(), &source_context);
756
757 let source_url = self
758 .core
759 .source_manager()
760 .get_source_url(source_name)
761 .ok_or_else(|| anyhow::anyhow!("Source '{}' not found", source_name))?;
762
763 let version_key = dep.get_version().map_or_else(|| "HEAD".to_string(), |v| v.to_string());
764 let group_key = format!("{}::{}", source_name, version_key);
765
766 let prepared = self.version_service.get_prepared_version(&group_key).ok_or_else(|| {
767 anyhow::anyhow!(
768 "Prepared state missing for source '{}' @ '{}'",
769 source_name,
770 version_key
771 )
772 })?;
773
774 let filename = Self::resolve_filename(dep);
775 let artifact_type_string = self.resolve_tool(dep, resource_type);
776 let artifact_type = artifact_type_string.as_str();
777
778 let installed_at = install_path_resolver::resolve_install_path(
779 self.core.manifest(),
780 dep,
781 artifact_type,
782 resource_type,
783 &filename,
784 )?;
785
786 let manifest_alias = self.resolve_manifest_alias(name, resource_type);
787
788 let applied_patches = lockfile_builder::get_patches_for_resource(
789 self.core.manifest(),
790 resource_type,
791 name,
792 manifest_alias.as_deref(),
793 );
794
795 let variant_inputs = lockfile_builder::VariantInputs::new(
796 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
797 );
798
799 Ok(LockedResource {
800 name: canonical_name,
801 source: Some(source_name.to_string()),
802 url: Some(source_url.clone()),
803 path: normalize_path_for_storage(dep.get_path()),
804 version: prepared.resolved_version.clone(),
805 resolved_commit: Some(prepared.resolved_commit.clone()),
806 checksum: String::new(),
807 installed_at,
808 dependencies: self.get_dependencies_for(
809 name,
810 Some(source_name),
811 resource_type,
812 Some(&artifact_type_string),
813 variant_inputs.hash(),
814 ),
815 resource_type,
816 tool: Some(artifact_type_string),
817 manifest_alias,
818 applied_patches,
819 install: dep.get_install(),
820 variant_inputs,
821 context_checksum: None,
822 })
823 }
824
825 async fn resolve_pattern_dependency(
829 &mut self,
830 name: &str,
831 dep: &ResourceDependency,
832 resource_type: ResourceType,
833 ) -> Result<Vec<LockedResource>> {
834 if !dep.is_pattern() {
835 return Err(anyhow::anyhow!(
836 "Expected pattern dependency but no glob characters found in path"
837 ));
838 }
839
840 if dep.is_local() {
841 self.resolve_local_pattern(name, dep, resource_type)
842 } else {
843 self.resolve_git_pattern(name, dep, resource_type).await
844 }
845 }
846
847 fn resolve_local_pattern(
849 &self,
850 name: &str,
851 dep: &ResourceDependency,
852 resource_type: ResourceType,
853 ) -> Result<Vec<LockedResource>> {
854 use crate::pattern::PatternResolver;
855 use crate::resolver::{lockfile_builder, path_resolver};
856
857 let pattern = dep.get_path();
858 let (base_path, pattern_str) = path_resolver::parse_pattern_base_path(pattern);
859 let pattern_resolver = PatternResolver::new();
860 let matches = pattern_resolver.resolve(&pattern_str, &base_path)?;
861
862 let artifact_type_string = self.resolve_tool(dep, resource_type);
863 let artifact_type = artifact_type_string.as_str();
864
865 let variant_inputs = lockfile_builder::VariantInputs::new(
867 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
868 );
869
870 let mut resources = Vec::new();
871 for matched_path in matches {
872 let resource_name = crate::pattern::extract_resource_name(&matched_path);
873 let full_relative_path =
874 path_resolver::construct_full_relative_path(&base_path, &matched_path);
875 let filename = path_resolver::extract_pattern_filename(&base_path, &matched_path);
876
877 let installed_at = path_resolver::resolve_install_path(
878 self.core.manifest(),
879 dep,
880 artifact_type,
881 resource_type,
882 &filename,
883 )?;
884
885 resources.push(LockedResource {
886 name: resource_name.clone(),
887 source: None,
888 url: None,
889 path: full_relative_path,
890 version: None,
891 resolved_commit: None,
892 checksum: String::new(),
893 installed_at,
894 dependencies: vec![],
895 resource_type,
896 tool: Some(artifact_type_string.clone()),
897 manifest_alias: Some(name.to_string()),
898 applied_patches: lockfile_builder::get_patches_for_resource(
899 self.core.manifest(),
900 resource_type,
901 &resource_name, Some(name), ),
904 install: dep.get_install(),
905 variant_inputs: variant_inputs.clone(),
906 context_checksum: None,
907 });
908 }
909
910 Ok(resources)
911 }
912
913 async fn resolve_git_pattern(
915 &mut self,
916 name: &str,
917 dep: &ResourceDependency,
918 resource_type: ResourceType,
919 ) -> Result<Vec<LockedResource>> {
920 use crate::pattern::PatternResolver;
921 use crate::resolver::{lockfile_builder, path_resolver};
922 use crate::utils::{
923 compute_relative_install_path, normalize_path, normalize_path_for_storage,
924 };
925
926 let pattern = dep.get_path();
927 let pattern_name = name;
928
929 let source_name = dep.get_source().ok_or_else(|| {
930 anyhow::anyhow!("Pattern dependency '{}' has no source specified", name)
931 })?;
932
933 let source_url = self
934 .core
935 .source_manager()
936 .get_source_url(source_name)
937 .ok_or_else(|| anyhow::anyhow!("Source '{}' not found", source_name))?;
938
939 let version_key = dep.get_version().map_or_else(|| "HEAD".to_string(), |v| v.to_string());
940 let group_key = format!("{}::{}", source_name, version_key);
941
942 let prepared = self.version_service.get_prepared_version(&group_key).ok_or_else(|| {
943 anyhow::anyhow!(
944 "Prepared state missing for source '{}' @ '{}'",
945 source_name,
946 version_key
947 )
948 })?;
949
950 let repo_path = Path::new(&prepared.worktree_path);
951 let pattern_resolver = PatternResolver::new();
952 let matches = pattern_resolver.resolve(pattern, repo_path)?;
953
954 let artifact_type_string = self.resolve_tool(dep, resource_type);
955 let artifact_type = artifact_type_string.as_str();
956
957 let variant_inputs = lockfile_builder::VariantInputs::new(
959 lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep),
960 );
961
962 let mut resources = Vec::new();
963 for matched_path in matches {
964 let resource_name = crate::pattern::extract_resource_name(&matched_path);
965
966 let installed_at = match resource_type {
968 ResourceType::Hook | ResourceType::McpServer => {
969 path_resolver::resolve_merge_target_path(
970 self.core.manifest(),
971 artifact_type,
972 resource_type,
973 )
974 }
975 _ => {
976 let artifact_path = self
977 .core
978 .manifest()
979 .get_artifact_resource_path(artifact_type, resource_type)
980 .ok_or_else(|| {
981 anyhow::anyhow!(
982 "Resource type '{}' is not supported by tool '{}'",
983 resource_type,
984 artifact_type
985 )
986 })?;
987
988 let dep_flatten = dep.get_flatten();
989 let tool_flatten = self
990 .core
991 .manifest()
992 .get_tool_config(artifact_type)
993 .and_then(|config| config.resources.get(resource_type.to_plural()))
994 .and_then(|resource_config| resource_config.flatten);
995
996 let flatten = dep_flatten.or(tool_flatten).unwrap_or(false);
997
998 let base_target = if let Some(custom_target) = dep.get_target() {
999 PathBuf::from(artifact_path.display().to_string())
1000 .join(custom_target.trim_start_matches('/'))
1001 } else {
1002 artifact_path.to_path_buf()
1003 };
1004
1005 let filename = repo_path.join(&matched_path).to_string_lossy().to_string();
1006 let relative_path =
1007 compute_relative_install_path(&base_target, Path::new(&filename), flatten);
1008 normalize_path_for_storage(normalize_path(&base_target.join(relative_path)))
1009 }
1010 };
1011
1012 resources.push(LockedResource {
1013 name: resource_name.clone(),
1014 source: Some(source_name.to_string()),
1015 url: Some(source_url.clone()),
1016 path: normalize_path_for_storage(matched_path.to_string_lossy().to_string()),
1017 version: prepared.resolved_version.clone(),
1018 resolved_commit: Some(prepared.resolved_commit.clone()),
1019 checksum: String::new(),
1020 installed_at,
1021 dependencies: vec![],
1022 resource_type,
1023 tool: Some(artifact_type_string.clone()),
1024 manifest_alias: Some(pattern_name.to_string()),
1025 applied_patches: lockfile_builder::get_patches_for_resource(
1026 self.core.manifest(),
1027 resource_type,
1028 &resource_name, Some(pattern_name), ),
1031 install: dep.get_install(),
1032 variant_inputs: variant_inputs.clone(),
1033 context_checksum: None,
1034 });
1035 }
1036
1037 Ok(resources)
1038 }
1039
1040 fn add_or_update_lockfile_entry(
1042 &self,
1043 lockfile: &mut LockFile,
1044 _name: &str,
1045 entry: LockedResource,
1046 ) {
1047 let resources = lockfile.get_resources_mut(&entry.resource_type);
1048
1049 if let Some(existing) =
1050 resources.iter_mut().find(|e| lockfile_builder::is_duplicate_entry(e, &entry))
1051 {
1052 let existing_is_direct = existing.manifest_alias.is_some();
1055 let new_is_direct = entry.manifest_alias.is_some();
1056
1057 if new_is_direct || !existing_is_direct {
1058 tracing::debug!(
1062 "Replacing {} (direct={}) with {} (direct={})",
1063 existing.name,
1064 existing_is_direct,
1065 entry.name,
1066 new_is_direct
1067 );
1068 *existing = entry;
1069 } else {
1070 tracing::debug!("Keeping direct {} over transitive {}", existing.name, entry.name);
1072 }
1073 } else {
1074 resources.push(entry);
1075 }
1076 }
1077
1078 fn add_version_to_dependencies(&self, lockfile: &mut LockFile) -> Result<()> {
1080 use crate::resolver::lockfile_builder;
1081
1082 lockfile_builder::add_version_to_all_dependencies(lockfile);
1083 Ok(())
1084 }
1085
1086 fn detect_target_conflicts(&self, lockfile: &LockFile) -> Result<()> {
1088 use crate::resolver::lockfile_builder;
1089
1090 lockfile_builder::detect_target_conflicts(lockfile)
1091 }
1092
1093 fn add_to_conflict_detector(
1095 &mut self,
1096 _name: &str,
1097 dep: &ResourceDependency,
1098 required_by: &str,
1099 ) {
1100 use crate::resolver::types as dependency_helpers;
1101
1102 if dep.is_local() {
1104 return;
1105 }
1106
1107 let resource_id = dependency_helpers::build_resource_id(dep);
1109
1110 let version = dep.get_version().unwrap_or("HEAD");
1112
1113 self.conflict_detector.add_requirement(&resource_id, required_by, version);
1115 }
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120 use super::*;
1121
1122 #[tokio::test]
1123 async fn test_resolver_creation() {
1124 let manifest = Manifest::default();
1125 let cache = Cache::new().unwrap();
1126 let resolver = DependencyResolver::new(manifest, cache).await;
1127 assert!(resolver.is_ok());
1128 }
1129
1130 #[tokio::test]
1131 async fn test_resolver_with_global() {
1132 let manifest = Manifest::default();
1133 let cache = Cache::new().unwrap();
1134 let resolver = DependencyResolver::new_with_global(manifest, cache).await;
1135 assert!(resolver.is_ok());
1136 }
1137}