1use std::collections::{HashMap, HashSet};
9use std::path::{Path, PathBuf};
10
11use anyhow::{Context, Result};
12
13use crate::core::ResourceType;
14use crate::lockfile::lockfile_dependency_ref::LockfileDependencyRef;
15use crate::manifest::{DetailedDependency, ResourceDependency};
16use crate::metadata::MetadataExtractor;
17use crate::utils;
18
19use super::dependency_graph::{DependencyGraph, DependencyNode};
20use super::pattern_expander::generate_dependency_name;
21use super::types::{
22 DependencyKey, TransitiveContext, apply_manifest_override, compute_dependency_variant_hash,
23};
24use super::version_resolver::{PreparedSourceVersion, VersionResolutionService};
25use super::{PatternExpansionService, ResourceFetchingService, is_file_relative_path};
26
27pub struct ResolutionServices<'a> {
29 pub version_service: &'a mut VersionResolutionService,
31 pub pattern_service: &'a mut PatternExpansionService,
33}
34
35#[allow(clippy::too_many_arguments)]
37async fn process_transitive_dependency_spec(
38 ctx: &TransitiveContext<'_>,
39 core: &super::ResolutionCore,
40 parent_dep: &ResourceDependency,
41 dep_resource_type: ResourceType,
42 parent_resource_type: ResourceType,
43 parent_name: &str,
44 dep_spec: &crate::manifest::DependencySpec,
45 version_service: &mut VersionResolutionService,
46 prepared_versions: &HashMap<String, PreparedSourceVersion>,
47) -> Result<(ResourceDependency, String)> {
48 let parent_file_path =
50 ResourceFetchingService::get_canonical_path(core, parent_dep, version_service)
51 .await
52 .with_context(|| {
53 format!(
54 "Failed to get parent path for transitive dependencies of '{}'",
55 parent_name
56 )
57 })?;
58
59 let trans_canonical = resolve_transitive_path(&parent_file_path, &dep_spec.path, parent_name)?;
61
62 let trans_dep = create_transitive_dependency(
64 ctx,
65 parent_dep,
66 dep_resource_type,
67 parent_resource_type,
68 parent_name,
69 dep_spec,
70 &parent_file_path,
71 &trans_canonical,
72 prepared_versions,
73 )
74 .await?;
75
76 let trans_name = if trans_dep.get_source().is_none() {
78 let manifest_dir = ctx
82 .base
83 .manifest
84 .manifest_dir
85 .as_ref()
86 .ok_or_else(|| anyhow::anyhow!("Manifest directory not available"))?;
87
88 let source_context = crate::resolver::source_context::SourceContext::local(manifest_dir);
89 generate_dependency_name(trans_dep.get_path(), &source_context)
90 } else {
91 let source_name = trans_dep
93 .get_source()
94 .ok_or_else(|| anyhow::anyhow!("Git dependency missing source name"))?;
95 let source_context = crate::resolver::source_context::SourceContext::remote(source_name);
96 generate_dependency_name(trans_dep.get_path(), &source_context)
97 };
98
99 Ok((trans_dep, trans_name))
100}
101
102fn resolve_transitive_path(
104 parent_file_path: &Path,
105 dep_path: &str,
106 parent_name: &str,
107) -> Result<PathBuf> {
108 let is_pattern = dep_path.contains('*') || dep_path.contains('?') || dep_path.contains('[');
110
111 if is_pattern {
112 let parent_dir = parent_file_path.parent().ok_or_else(|| {
114 anyhow::anyhow!(
115 "Failed to resolve transitive dependency '{}' for '{}': parent file has no directory",
116 dep_path,
117 parent_name
118 )
119 })?;
120 let resolved = parent_dir.join(dep_path);
121
122 let mut result = PathBuf::new();
124 for component in resolved.components() {
125 match component {
126 std::path::Component::RootDir => result.push(component),
127 std::path::Component::ParentDir => {
128 result.pop();
129 }
130 std::path::Component::CurDir => {}
131 _ => result.push(component),
132 }
133 }
134 Ok(result)
135 } else if is_file_relative_path(dep_path) || !dep_path.contains('/') {
136 let parent_dir = parent_file_path.parent().ok_or_else(|| {
139 anyhow::anyhow!(
140 "Failed to resolve transitive dependency '{}' for '{}': parent file has no directory",
141 dep_path,
142 parent_name
143 )
144 })?;
145
146 let resolved = parent_dir.join(dep_path);
147 resolved.canonicalize().map_err(|e| {
148 let file_error = crate::core::file_error::FileOperationError::new(
150 crate::core::file_error::FileOperationContext::new(
151 crate::core::file_error::FileOperation::Canonicalize,
152 &resolved,
153 format!("resolving transitive dependency '{}' for '{}'", dep_path, parent_name),
154 "transitive_resolver::resolve_transitive_path",
155 ),
156 e,
157 );
158 anyhow::Error::from(file_error)
159 })
160 } else {
161 resolve_repo_relative_path(parent_file_path, dep_path, parent_name)
163 }
164}
165
166fn resolve_repo_relative_path(
168 parent_file_path: &Path,
169 dep_path: &str,
170 parent_name: &str,
171) -> Result<PathBuf> {
172 let repo_root = parent_file_path
174 .ancestors()
175 .find(|p| {
176 p.file_name().and_then(|n| n.to_str()).map(|s| s.contains('_')).unwrap_or(false)
178 })
179 .or_else(|| parent_file_path.ancestors().nth(2)) .ok_or_else(|| {
181 anyhow::anyhow!(
182 "Failed to find repository root for transitive dependency '{}'",
183 dep_path
184 )
185 })?;
186
187 let full_path = repo_root.join(dep_path);
188 full_path.canonicalize().with_context(|| {
189 format!(
190 "Failed to resolve repo-relative transitive dependency '{}' for '{}': {} (repo root: {})",
191 dep_path,
192 parent_name,
193 full_path.display(),
194 repo_root.display()
195 )
196 })
197}
198
199#[allow(clippy::too_many_arguments)]
201async fn create_transitive_dependency(
202 ctx: &TransitiveContext<'_>,
203 parent_dep: &ResourceDependency,
204 dep_resource_type: ResourceType,
205 parent_resource_type: ResourceType,
206 parent_name: &str,
207 dep_spec: &crate::manifest::DependencySpec,
208 parent_file_path: &Path,
209 trans_canonical: &Path,
210 prepared_versions: &HashMap<String, PreparedSourceVersion>,
211) -> Result<ResourceDependency> {
212 use super::types::{OverrideKey, compute_dependency_variant_hash, normalize_lookup_path};
213
214 let mut dep = if parent_dep.get_source().is_none() {
216 create_path_only_transitive_dep(
217 ctx,
218 parent_dep,
219 dep_resource_type,
220 parent_resource_type,
221 dep_spec,
222 trans_canonical,
223 )?
224 } else {
225 create_git_backed_transitive_dep(
226 ctx,
227 parent_dep,
228 dep_resource_type,
229 parent_resource_type,
230 parent_name,
231 dep_spec,
232 parent_file_path,
233 trans_canonical,
234 prepared_versions,
235 )
236 .await?
237 };
238
239 let normalized_path = normalize_lookup_path(dep.get_path());
241 let source = dep.get_source().map(std::string::ToString::to_string);
242
243 let tool = dep
245 .get_tool()
246 .map(str::to_string)
247 .unwrap_or_else(|| ctx.base.manifest.get_default_tool(dep_resource_type));
248
249 let variant_hash = compute_dependency_variant_hash(&dep);
250
251 let override_key = OverrideKey {
252 resource_type: dep_resource_type,
253 normalized_path: normalized_path.clone(),
254 source,
255 tool,
256 variant_hash,
257 };
258
259 if let Some(override_info) = ctx.manifest_overrides.get(&override_key) {
261 apply_manifest_override(&mut dep, override_info, &normalized_path);
262 }
263
264 Ok(dep)
265}
266
267fn create_path_only_transitive_dep(
269 ctx: &TransitiveContext<'_>,
270 parent_dep: &ResourceDependency,
271 dep_resource_type: ResourceType,
272 parent_resource_type: ResourceType,
273 dep_spec: &crate::manifest::DependencySpec,
274 trans_canonical: &Path,
275) -> Result<ResourceDependency> {
276 let manifest_dir = ctx.base.manifest.manifest_dir.as_ref().ok_or_else(|| {
277 anyhow::anyhow!("Manifest directory not available for path-only transitive dep")
278 })?;
279
280 let dep_path_str = match manifest_dir.canonicalize() {
282 Ok(canonical_manifest) => {
283 utils::compute_relative_path(&canonical_manifest, trans_canonical)
284 }
285 Err(e) => {
286 eprintln!(
287 "Warning: Could not canonicalize manifest directory {}: {}. Using non-canonical path.",
288 manifest_dir.display(),
289 e
290 );
291 utils::compute_relative_path(manifest_dir, trans_canonical)
292 }
293 };
294
295 let trans_tool = determine_transitive_tool(
297 ctx,
298 parent_dep,
299 dep_spec,
300 parent_resource_type,
301 dep_resource_type,
302 );
303
304 Ok(ResourceDependency::Detailed(Box::new(DetailedDependency {
305 source: None,
306 path: utils::normalize_path_for_storage(dep_path_str),
307 version: None,
308 branch: None,
309 rev: None,
310 command: None,
311 args: None,
312 target: None,
313 filename: None,
314 dependencies: None,
315 tool: trans_tool,
316 flatten: None,
317 install: dep_spec.install.or(Some(true)),
318 template_vars: Some(super::lockfile_builder::build_merged_variant_inputs(
319 ctx.base.manifest,
320 parent_dep,
321 )),
322 })))
323}
324
325#[allow(clippy::too_many_arguments)]
327async fn create_git_backed_transitive_dep(
328 ctx: &TransitiveContext<'_>,
329 parent_dep: &ResourceDependency,
330 dep_resource_type: ResourceType,
331 parent_resource_type: ResourceType,
332 _parent_name: &str,
333 dep_spec: &crate::manifest::DependencySpec,
334 parent_file_path: &Path,
335 trans_canonical: &Path,
336 _prepared_versions: &HashMap<String, PreparedSourceVersion>,
337) -> Result<ResourceDependency> {
338 let source_name = parent_dep
339 .get_source()
340 .ok_or_else(|| anyhow::anyhow!("Expected source for Git-backed dependency"))?;
341 let source_url = ctx
342 .base
343 .source_manager
344 .get_source_url(source_name)
345 .ok_or_else(|| anyhow::anyhow!("Source '{source_name}' not found"))?;
346
347 let repo_relative = if utils::is_local_path(&source_url) {
349 strip_local_source_prefix(&source_url, trans_canonical)?
350 } else {
351 strip_git_worktree_prefix_from_parent(parent_file_path, trans_canonical)?
353 };
354
355 let trans_tool = determine_transitive_tool(
357 ctx,
358 parent_dep,
359 dep_spec,
360 parent_resource_type,
361 dep_resource_type,
362 );
363
364 Ok(ResourceDependency::Detailed(Box::new(DetailedDependency {
365 source: Some(source_name.to_string()),
366 path: utils::normalize_path_for_storage(repo_relative.to_string_lossy().to_string()),
367 version: dep_spec
368 .version
369 .clone()
370 .or_else(|| parent_dep.get_version().map(|v| v.to_string())),
371 branch: None,
372 rev: None,
373 command: None,
374 args: None,
375 target: None,
376 filename: None,
377 dependencies: None,
378 tool: trans_tool,
379 flatten: None,
380 install: dep_spec.install.or(Some(true)),
381 template_vars: Some(super::lockfile_builder::build_merged_variant_inputs(
382 ctx.base.manifest,
383 parent_dep,
384 )),
385 })))
386}
387
388fn strip_local_source_prefix(source_url: &str, trans_canonical: &Path) -> Result<PathBuf> {
390 let source_path = PathBuf::from(source_url).canonicalize()?;
391
392 let trans_str = trans_canonical.to_string_lossy();
394 let is_pattern = trans_str.contains('*') || trans_str.contains('?') || trans_str.contains('[');
395
396 if is_pattern {
397 let parent_dir = trans_canonical.parent().ok_or_else(|| {
399 anyhow::anyhow!("Pattern path has no parent directory: {}", trans_canonical.display())
400 })?;
401 let filename = trans_canonical.file_name().ok_or_else(|| {
402 anyhow::anyhow!("Pattern path has no filename: {}", trans_canonical.display())
403 })?;
404
405 let canonical_dir = parent_dir.canonicalize().with_context(|| {
407 format!("Failed to canonicalize pattern directory: {}", parent_dir.display())
408 })?;
409
410 let canonical_pattern = canonical_dir.join(filename);
412
413 canonical_pattern
415 .strip_prefix(&source_path)
416 .with_context(|| {
417 format!(
418 "Transitive pattern dep outside parent's source: {} not under {}",
419 canonical_pattern.display(),
420 source_path.display()
421 )
422 })
423 .map(|p| p.to_path_buf())
424 } else {
425 trans_canonical
426 .strip_prefix(&source_path)
427 .with_context(|| {
428 format!(
429 "Transitive dep resolved outside parent's source directory: {} not under {}",
430 trans_canonical.display(),
431 source_path.display()
432 )
433 })
434 .map(|p| p.to_path_buf())
435 }
436}
437
438fn strip_git_worktree_prefix_from_parent(
441 parent_file_path: &Path,
442 trans_canonical: &Path,
443) -> Result<PathBuf> {
444 let worktree_root = parent_file_path
447 .ancestors()
448 .find(|p| {
449 p.file_name()
450 .and_then(|n| n.to_str())
451 .map(|s| {
452 s.contains('_')
454 })
455 .unwrap_or(false)
456 })
457 .ok_or_else(|| {
458 anyhow::anyhow!(
459 "Failed to find worktree root from parent file: {}",
460 parent_file_path.display()
461 )
462 })?;
463
464 let canonical_worktree = worktree_root.canonicalize().with_context(|| {
466 format!("Failed to canonicalize worktree root: {}", worktree_root.display())
467 })?;
468
469 let trans_str = trans_canonical.to_string_lossy();
471 let is_pattern = trans_str.contains('*') || trans_str.contains('?') || trans_str.contains('[');
472
473 if is_pattern {
474 let parent_dir = trans_canonical.parent().ok_or_else(|| {
476 anyhow::anyhow!("Pattern path has no parent directory: {}", trans_canonical.display())
477 })?;
478 let filename = trans_canonical.file_name().ok_or_else(|| {
479 anyhow::anyhow!("Pattern path has no filename: {}", trans_canonical.display())
480 })?;
481
482 let canonical_dir = parent_dir.canonicalize().with_context(|| {
484 format!("Failed to canonicalize pattern directory: {}", parent_dir.display())
485 })?;
486
487 let canonical_pattern = canonical_dir.join(filename);
489
490 canonical_pattern
492 .strip_prefix(&canonical_worktree)
493 .with_context(|| {
494 format!(
495 "Transitive pattern dep outside parent's worktree: {} not under {}",
496 canonical_pattern.display(),
497 canonical_worktree.display()
498 )
499 })
500 .map(|p| p.to_path_buf())
501 } else {
502 trans_canonical
503 .strip_prefix(&canonical_worktree)
504 .with_context(|| {
505 format!(
506 "Transitive dep outside parent's worktree: {} not under {}",
507 trans_canonical.display(),
508 canonical_worktree.display()
509 )
510 })
511 .map(|p| p.to_path_buf())
512 }
513}
514
515fn determine_transitive_tool(
517 ctx: &TransitiveContext<'_>,
518 parent_dep: &ResourceDependency,
519 dep_spec: &crate::manifest::DependencySpec,
520 parent_resource_type: ResourceType,
521 dep_resource_type: ResourceType,
522) -> Option<String> {
523 if let Some(explicit_tool) = &dep_spec.tool {
524 Some(explicit_tool.clone())
525 } else {
526 let parent_tool = parent_dep
527 .get_tool()
528 .map(str::to_string)
529 .unwrap_or_else(|| ctx.base.manifest.get_default_tool(parent_resource_type));
530 if ctx.base.manifest.is_resource_supported(&parent_tool, dep_resource_type) {
531 Some(parent_tool)
532 } else {
533 Some(ctx.base.manifest.get_default_tool(dep_resource_type))
534 }
535 }
536}
537
538fn add_to_conflict_detector(
540 ctx: &mut TransitiveContext<'_>,
541 name: &str,
542 dep: &ResourceDependency,
543 requester: &str,
544) {
545 if let Some(version) = dep.get_version() {
546 ctx.conflict_detector.add_requirement(name, requester, version);
547 }
548}
549
550fn build_ordered_result(
552 all_deps: HashMap<DependencyKey, ResourceDependency>,
553 ordered_nodes: Vec<DependencyNode>,
554) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
555 let mut result = Vec::new();
556 let mut added_keys = HashSet::new();
557
558 tracing::debug!(
559 "Transitive resolution - topological order has {} nodes, all_deps has {} entries",
560 ordered_nodes.len(),
561 all_deps.len()
562 );
563
564 for node in ordered_nodes {
565 tracing::debug!(
566 "Processing ordered node: {}/{} (source: {:?})",
567 node.resource_type,
568 node.name,
569 node.source
570 );
571
572 for (key, dep) in &all_deps {
574 if key.0 == node.resource_type && key.1 == node.name && key.2 == node.source {
575 tracing::debug!(
576 " -> Found match in all_deps, adding to result with type {:?}",
577 node.resource_type
578 );
579 result.push((node.name.clone(), dep.clone(), node.resource_type));
580 added_keys.insert(key.clone());
581 break;
582 }
583 }
584 }
585
586 for (key, dep) in all_deps {
588 if !added_keys.contains(&key) && !dep.is_pattern() {
589 tracing::debug!(
590 "Adding non-graph dependency: {}/{} (source: {:?}) with type {:?}",
591 key.0,
592 key.1,
593 key.2,
594 key.0
595 );
596 result.push((key.1.clone(), dep.clone(), key.0));
597 }
598 }
599
600 tracing::debug!("Transitive resolution returning {} dependencies", result.len());
601
602 Ok(result)
603}
604
605pub fn group_key(source: &str, version: &str) -> String {
607 format!("{source}::{version}")
608}
609
610pub async fn resolve_with_services(
615 ctx: &mut TransitiveContext<'_>,
616 core: &super::ResolutionCore,
617 base_deps: &[(String, ResourceDependency, ResourceType)],
618 enable_transitive: bool,
619 prepared_versions: &HashMap<String, PreparedSourceVersion>,
620 pattern_alias_map: &mut HashMap<(ResourceType, String), String>,
621 services: &mut ResolutionServices<'_>,
622) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
623 ctx.dependency_map.clear();
625
626 if !enable_transitive {
627 return Ok(base_deps.to_vec());
628 }
629
630 let mut graph = DependencyGraph::new();
631 let mut all_deps: HashMap<DependencyKey, ResourceDependency> = HashMap::new();
632 let mut processed: HashSet<DependencyKey> = HashSet::new();
633 let mut queue: Vec<(String, ResourceDependency, Option<ResourceType>, String)> = Vec::new();
634
635 for (name, dep, resource_type) in base_deps {
637 let source = dep.get_source().map(std::string::ToString::to_string);
638 let tool = dep.get_tool().map(std::string::ToString::to_string);
639
640 let merged_variant_inputs =
643 super::lockfile_builder::build_merged_variant_inputs(ctx.base.manifest, dep);
644 let variant_hash = crate::utils::compute_variant_inputs_hash(&merged_variant_inputs)
645 .unwrap_or_else(|_| crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string());
646
647 tracing::debug!(
648 "[DEBUG] Adding base dep to queue: '{}' (type: {:?}, source: {:?}, tool: {:?}, is_local: {})",
649 name,
650 resource_type,
651 source,
652 tool,
653 dep.is_local()
654 );
655 queue.push((name.clone(), dep.clone(), Some(*resource_type), variant_hash.clone()));
657 all_deps.insert((*resource_type, name.clone(), source, tool, variant_hash), dep.clone());
658 }
659
660 while let Some((name, dep, resource_type, variant_hash)) = queue.pop() {
662 let source = dep.get_source().map(std::string::ToString::to_string);
663 let tool = dep.get_tool().map(std::string::ToString::to_string);
664
665 let resource_type =
666 resource_type.expect("resource_type should always be threaded through queue");
667 let key = (resource_type, name.clone(), source.clone(), tool.clone(), variant_hash.clone());
668
669 tracing::debug!(
670 "[TRANSITIVE] Processing: '{}' (type: {:?}, source: {:?})",
671 name,
672 resource_type,
673 source
674 );
675
676 if let Some(current_dep) = all_deps.get(&key) {
678 if current_dep.get_version() != dep.get_version() {
679 tracing::debug!("[TRANSITIVE] Skipped stale: '{}'", name);
680 continue;
681 }
682 }
683
684 if processed.contains(&key) {
685 tracing::debug!("[TRANSITIVE] Already processed: '{}'", name);
686 continue;
687 }
688
689 processed.insert(key.clone());
690
691 if dep.is_pattern() {
693 tracing::debug!("[TRANSITIVE] Expanding pattern: '{}'", name);
694 match services
695 .pattern_service
696 .expand_pattern(core, &dep, resource_type, services.version_service)
697 .await
698 {
699 Ok(concrete_deps) => {
700 for (concrete_name, concrete_dep) in concrete_deps {
701 pattern_alias_map
702 .insert((resource_type, concrete_name.clone()), name.clone());
703
704 let concrete_source =
705 concrete_dep.get_source().map(std::string::ToString::to_string);
706 let concrete_tool =
707 concrete_dep.get_tool().map(std::string::ToString::to_string);
708 let concrete_variant_hash = compute_dependency_variant_hash(&concrete_dep);
709 let concrete_key = (
710 resource_type,
711 concrete_name.clone(),
712 concrete_source,
713 concrete_tool,
714 concrete_variant_hash.clone(),
715 );
716
717 if let std::collections::hash_map::Entry::Vacant(e) =
718 all_deps.entry(concrete_key)
719 {
720 e.insert(concrete_dep.clone());
721 queue.push((
722 concrete_name,
723 concrete_dep,
724 Some(resource_type),
725 concrete_variant_hash,
726 ));
727 }
728 }
729 }
730 Err(e) => {
731 anyhow::bail!("Failed to expand pattern '{}': {}", dep.get_path(), e);
732 }
733 }
734 continue;
735 }
736
737 let content = ResourceFetchingService::fetch_content(core, &dep, services.version_service)
739 .await
740 .with_context(|| {
741 format!(
742 "Failed to fetch resource '{}' ({}) for transitive deps",
743 name,
744 dep.get_path()
745 )
746 })?;
747
748 tracing::debug!("[TRANSITIVE] Fetched content for '{}' ({} bytes)", name, content.len());
749
750 let variant_inputs_value =
753 super::lockfile_builder::build_merged_variant_inputs(ctx.base.manifest, &dep);
754 let variant_inputs = Some(&variant_inputs_value);
755
756 let path = PathBuf::from(dep.get_path());
758 let metadata = MetadataExtractor::extract(
759 &path,
760 &content,
761 variant_inputs,
762 ctx.base.operation_context.map(|arc| arc.as_ref()),
763 )?;
764
765 tracing::debug!(
766 "[DEBUG] Extracted metadata for '{}': has_deps={}",
767 name,
768 metadata.get_dependencies().is_some()
769 );
770
771 if let Some(deps_map) = metadata.get_dependencies() {
773 tracing::debug!(
774 "[DEBUG] Found {} dependency type(s) for '{}': {:?}",
775 deps_map.len(),
776 name,
777 deps_map.keys().collect::<Vec<_>>()
778 );
779
780 for (dep_resource_type_str, dep_specs) in deps_map {
781 let dep_resource_type: ResourceType =
782 dep_resource_type_str.parse().unwrap_or(ResourceType::Snippet);
783
784 for dep_spec in dep_specs {
785 let (trans_dep, trans_name) = process_transitive_dependency_spec(
787 ctx,
788 core,
789 &dep,
790 dep_resource_type,
791 resource_type,
792 &name,
793 dep_spec,
794 services.version_service,
795 prepared_versions,
796 )
797 .await?;
798
799 let trans_source = trans_dep.get_source().map(std::string::ToString::to_string);
800 let trans_tool = trans_dep.get_tool().map(std::string::ToString::to_string);
801 let trans_variant_hash = compute_dependency_variant_hash(&trans_dep);
802
803 if let Some(custom_name) = &dep_spec.name {
805 let trans_key = (
806 dep_resource_type,
807 trans_name.clone(),
808 trans_source.clone(),
809 trans_tool.clone(),
810 trans_variant_hash.clone(),
811 );
812 ctx.transitive_custom_names.insert(trans_key, custom_name.clone());
813 tracing::debug!(
814 "Storing custom name '{}' for transitive dep '{}'",
815 custom_name,
816 trans_name
817 );
818 }
819
820 let from_node =
822 DependencyNode::with_source(resource_type, &name, source.clone());
823 let to_node = DependencyNode::with_source(
824 dep_resource_type,
825 &trans_name,
826 trans_source.clone(),
827 );
828 graph.add_dependency(from_node, to_node);
829
830 let from_key = (
832 resource_type,
833 name.clone(),
834 source.clone(),
835 tool.clone(),
836 variant_hash.clone(),
837 );
838 let dep_ref =
839 LockfileDependencyRef::local(dep_resource_type, trans_name.clone(), None)
840 .to_string();
841 tracing::debug!(
842 "[DEBUG] Adding to dependency_map: parent='{}' (type={:?}, source={:?}, tool={:?}, hash={}), child='{}' (type={:?})",
843 name,
844 resource_type,
845 source,
846 tool,
847 &variant_hash[..8],
848 dep_ref,
849 dep_resource_type
850 );
851 ctx.dependency_map.entry(from_key).or_default().push(dep_ref);
852
853 add_to_conflict_detector(ctx, &trans_name, &trans_dep, &name);
855
856 let trans_key = (
858 dep_resource_type,
859 trans_name.clone(),
860 trans_source.clone(),
861 trans_tool.clone(),
862 trans_variant_hash.clone(),
863 );
864
865 tracing::debug!(
866 "[TRANSITIVE] Found transitive dep '{}' (type: {:?}, tool: {:?}, parent: {})",
867 trans_name,
868 dep_resource_type,
869 trans_tool,
870 name
871 );
872
873 if let std::collections::hash_map::Entry::Vacant(e) = all_deps.entry(trans_key)
875 {
876 tracing::debug!(
878 "Adding transitive dep '{}' (parent: {})",
879 trans_name,
880 name
881 );
882 e.insert(trans_dep.clone());
883 queue.push((
884 trans_name,
885 trans_dep,
886 Some(dep_resource_type),
887 trans_variant_hash,
888 ));
889 } else {
890 tracing::debug!(
892 "[TRANSITIVE] Skipping duplicate transitive dep '{}' (already processed)",
893 trans_name
894 );
895 }
896 }
897 }
898 }
899 }
900
901 graph.detect_cycles()?;
903
904 let ordered_nodes = graph.topological_order()?;
906
907 build_ordered_result(all_deps, ordered_nodes)
909}