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_canonical.to_string_lossy(), &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().with_context(|| {
148 format!("Failed to resolve transitive dependency '{}' for '{}'", dep_path, parent_name)
149 })
150 } else {
151 resolve_repo_relative_path(parent_file_path, dep_path, parent_name)
153 }
154}
155
156fn resolve_repo_relative_path(
158 parent_file_path: &Path,
159 dep_path: &str,
160 parent_name: &str,
161) -> Result<PathBuf> {
162 let repo_root = parent_file_path
164 .ancestors()
165 .find(|p| {
166 p.file_name().and_then(|n| n.to_str()).map(|s| s.contains('_')).unwrap_or(false)
168 })
169 .or_else(|| parent_file_path.ancestors().nth(2)) .ok_or_else(|| {
171 anyhow::anyhow!(
172 "Failed to find repository root for transitive dependency '{}'",
173 dep_path
174 )
175 })?;
176
177 let full_path = repo_root.join(dep_path);
178 full_path.canonicalize().with_context(|| {
179 format!(
180 "Failed to resolve repo-relative transitive dependency '{}' for '{}': {} (repo root: {})",
181 dep_path,
182 parent_name,
183 full_path.display(),
184 repo_root.display()
185 )
186 })
187}
188
189#[allow(clippy::too_many_arguments)]
191async fn create_transitive_dependency(
192 ctx: &TransitiveContext<'_>,
193 parent_dep: &ResourceDependency,
194 dep_resource_type: ResourceType,
195 parent_resource_type: ResourceType,
196 parent_name: &str,
197 dep_spec: &crate::manifest::DependencySpec,
198 parent_file_path: &Path,
199 trans_canonical: &Path,
200 prepared_versions: &HashMap<String, PreparedSourceVersion>,
201) -> Result<ResourceDependency> {
202 use super::types::{OverrideKey, compute_dependency_variant_hash, normalize_lookup_path};
203
204 let mut dep = if parent_dep.get_source().is_none() {
206 create_path_only_transitive_dep(
207 ctx,
208 parent_dep,
209 dep_resource_type,
210 parent_resource_type,
211 dep_spec,
212 trans_canonical,
213 )?
214 } else {
215 create_git_backed_transitive_dep(
216 ctx,
217 parent_dep,
218 dep_resource_type,
219 parent_resource_type,
220 parent_name,
221 dep_spec,
222 parent_file_path,
223 trans_canonical,
224 prepared_versions,
225 )
226 .await?
227 };
228
229 let normalized_path = normalize_lookup_path(dep.get_path());
231 let source = dep.get_source().map(std::string::ToString::to_string);
232
233 let tool = dep
235 .get_tool()
236 .map(str::to_string)
237 .unwrap_or_else(|| ctx.base.manifest.get_default_tool(dep_resource_type));
238
239 let variant_hash = compute_dependency_variant_hash(&dep);
240
241 let override_key = OverrideKey {
242 resource_type: dep_resource_type,
243 normalized_path: normalized_path.clone(),
244 source,
245 tool,
246 variant_hash,
247 };
248
249 if let Some(override_info) = ctx.manifest_overrides.get(&override_key) {
251 apply_manifest_override(&mut dep, override_info, &normalized_path);
252 }
253
254 Ok(dep)
255}
256
257fn create_path_only_transitive_dep(
259 ctx: &TransitiveContext<'_>,
260 parent_dep: &ResourceDependency,
261 dep_resource_type: ResourceType,
262 parent_resource_type: ResourceType,
263 dep_spec: &crate::manifest::DependencySpec,
264 trans_canonical: &Path,
265) -> Result<ResourceDependency> {
266 let manifest_dir = ctx.base.manifest.manifest_dir.as_ref().ok_or_else(|| {
267 anyhow::anyhow!("Manifest directory not available for path-only transitive dep")
268 })?;
269
270 let dep_path_str = match manifest_dir.canonicalize() {
272 Ok(canonical_manifest) => {
273 utils::compute_relative_path(&canonical_manifest, trans_canonical)
274 }
275 Err(e) => {
276 eprintln!(
277 "Warning: Could not canonicalize manifest directory {}: {}. Using non-canonical path.",
278 manifest_dir.display(),
279 e
280 );
281 utils::compute_relative_path(manifest_dir, trans_canonical)
282 }
283 };
284
285 let trans_tool = determine_transitive_tool(
287 ctx,
288 parent_dep,
289 dep_spec,
290 parent_resource_type,
291 dep_resource_type,
292 );
293
294 Ok(ResourceDependency::Detailed(Box::new(DetailedDependency {
295 source: None,
296 path: utils::normalize_path_for_storage(dep_path_str),
297 version: None,
298 branch: None,
299 rev: None,
300 command: None,
301 args: None,
302 target: None,
303 filename: None,
304 dependencies: None,
305 tool: trans_tool,
306 flatten: None,
307 install: dep_spec.install.or(Some(true)),
308 template_vars: Some(super::lockfile_builder::build_merged_variant_inputs(
309 ctx.base.manifest,
310 parent_dep,
311 )),
312 })))
313}
314
315#[allow(clippy::too_many_arguments)]
317async fn create_git_backed_transitive_dep(
318 ctx: &TransitiveContext<'_>,
319 parent_dep: &ResourceDependency,
320 dep_resource_type: ResourceType,
321 parent_resource_type: ResourceType,
322 _parent_name: &str,
323 dep_spec: &crate::manifest::DependencySpec,
324 parent_file_path: &Path,
325 trans_canonical: &Path,
326 _prepared_versions: &HashMap<String, PreparedSourceVersion>,
327) -> Result<ResourceDependency> {
328 let source_name = parent_dep
329 .get_source()
330 .ok_or_else(|| anyhow::anyhow!("Expected source for Git-backed dependency"))?;
331 let source_url = ctx
332 .base
333 .source_manager
334 .get_source_url(source_name)
335 .ok_or_else(|| anyhow::anyhow!("Source '{source_name}' not found"))?;
336
337 let repo_relative = if utils::is_local_path(&source_url) {
339 strip_local_source_prefix(&source_url, trans_canonical)?
340 } else {
341 strip_git_worktree_prefix_from_parent(parent_file_path, trans_canonical)?
343 };
344
345 let trans_tool = determine_transitive_tool(
347 ctx,
348 parent_dep,
349 dep_spec,
350 parent_resource_type,
351 dep_resource_type,
352 );
353
354 Ok(ResourceDependency::Detailed(Box::new(DetailedDependency {
355 source: Some(source_name.to_string()),
356 path: utils::normalize_path_for_storage(repo_relative.to_string_lossy().to_string()),
357 version: dep_spec
358 .version
359 .clone()
360 .or_else(|| parent_dep.get_version().map(|v| v.to_string())),
361 branch: None,
362 rev: None,
363 command: None,
364 args: None,
365 target: None,
366 filename: None,
367 dependencies: None,
368 tool: trans_tool,
369 flatten: None,
370 install: dep_spec.install.or(Some(true)),
371 template_vars: Some(super::lockfile_builder::build_merged_variant_inputs(
372 ctx.base.manifest,
373 parent_dep,
374 )),
375 })))
376}
377
378fn strip_local_source_prefix(source_url: &str, trans_canonical: &Path) -> Result<PathBuf> {
380 let source_path = PathBuf::from(source_url).canonicalize()?;
381
382 let trans_str = trans_canonical.to_string_lossy();
384 let is_pattern = trans_str.contains('*') || trans_str.contains('?') || trans_str.contains('[');
385
386 if is_pattern {
387 let parent_dir = trans_canonical.parent().ok_or_else(|| {
389 anyhow::anyhow!("Pattern path has no parent directory: {}", trans_canonical.display())
390 })?;
391 let filename = trans_canonical.file_name().ok_or_else(|| {
392 anyhow::anyhow!("Pattern path has no filename: {}", trans_canonical.display())
393 })?;
394
395 let canonical_dir = parent_dir.canonicalize().with_context(|| {
397 format!("Failed to canonicalize pattern directory: {}", parent_dir.display())
398 })?;
399
400 let canonical_pattern = canonical_dir.join(filename);
402
403 canonical_pattern
405 .strip_prefix(&source_path)
406 .with_context(|| {
407 format!(
408 "Transitive pattern dep outside parent's source: {} not under {}",
409 canonical_pattern.display(),
410 source_path.display()
411 )
412 })
413 .map(|p| p.to_path_buf())
414 } else {
415 trans_canonical
416 .strip_prefix(&source_path)
417 .with_context(|| {
418 format!(
419 "Transitive dep resolved outside parent's source directory: {} not under {}",
420 trans_canonical.display(),
421 source_path.display()
422 )
423 })
424 .map(|p| p.to_path_buf())
425 }
426}
427
428fn strip_git_worktree_prefix_from_parent(
431 parent_file_path: &Path,
432 trans_canonical: &Path,
433) -> Result<PathBuf> {
434 let worktree_root = parent_file_path
437 .ancestors()
438 .find(|p| {
439 p.file_name()
440 .and_then(|n| n.to_str())
441 .map(|s| {
442 s.contains('_')
444 })
445 .unwrap_or(false)
446 })
447 .ok_or_else(|| {
448 anyhow::anyhow!(
449 "Failed to find worktree root from parent file: {}",
450 parent_file_path.display()
451 )
452 })?;
453
454 let canonical_worktree = worktree_root.canonicalize().with_context(|| {
456 format!("Failed to canonicalize worktree root: {}", worktree_root.display())
457 })?;
458
459 let trans_str = trans_canonical.to_string_lossy();
461 let is_pattern = trans_str.contains('*') || trans_str.contains('?') || trans_str.contains('[');
462
463 if is_pattern {
464 let parent_dir = trans_canonical.parent().ok_or_else(|| {
466 anyhow::anyhow!("Pattern path has no parent directory: {}", trans_canonical.display())
467 })?;
468 let filename = trans_canonical.file_name().ok_or_else(|| {
469 anyhow::anyhow!("Pattern path has no filename: {}", trans_canonical.display())
470 })?;
471
472 let canonical_dir = parent_dir.canonicalize().with_context(|| {
474 format!("Failed to canonicalize pattern directory: {}", parent_dir.display())
475 })?;
476
477 let canonical_pattern = canonical_dir.join(filename);
479
480 canonical_pattern
482 .strip_prefix(&canonical_worktree)
483 .with_context(|| {
484 format!(
485 "Transitive pattern dep outside parent's worktree: {} not under {}",
486 canonical_pattern.display(),
487 canonical_worktree.display()
488 )
489 })
490 .map(|p| p.to_path_buf())
491 } else {
492 trans_canonical
493 .strip_prefix(&canonical_worktree)
494 .with_context(|| {
495 format!(
496 "Transitive dep outside parent's worktree: {} not under {}",
497 trans_canonical.display(),
498 canonical_worktree.display()
499 )
500 })
501 .map(|p| p.to_path_buf())
502 }
503}
504
505fn determine_transitive_tool(
507 ctx: &TransitiveContext<'_>,
508 parent_dep: &ResourceDependency,
509 dep_spec: &crate::manifest::DependencySpec,
510 parent_resource_type: ResourceType,
511 dep_resource_type: ResourceType,
512) -> Option<String> {
513 if let Some(explicit_tool) = &dep_spec.tool {
514 Some(explicit_tool.clone())
515 } else {
516 let parent_tool = parent_dep
517 .get_tool()
518 .map(str::to_string)
519 .unwrap_or_else(|| ctx.base.manifest.get_default_tool(parent_resource_type));
520 if ctx.base.manifest.is_resource_supported(&parent_tool, dep_resource_type) {
521 Some(parent_tool)
522 } else {
523 Some(ctx.base.manifest.get_default_tool(dep_resource_type))
524 }
525 }
526}
527
528fn add_to_conflict_detector(
530 ctx: &mut TransitiveContext<'_>,
531 name: &str,
532 dep: &ResourceDependency,
533 requester: &str,
534) {
535 if let Some(version) = dep.get_version() {
536 ctx.conflict_detector.add_requirement(name, requester, version);
537 }
538}
539
540fn build_ordered_result(
542 all_deps: HashMap<DependencyKey, ResourceDependency>,
543 ordered_nodes: Vec<DependencyNode>,
544) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
545 let mut result = Vec::new();
546 let mut added_keys = HashSet::new();
547
548 tracing::debug!(
549 "Transitive resolution - topological order has {} nodes, all_deps has {} entries",
550 ordered_nodes.len(),
551 all_deps.len()
552 );
553
554 for node in ordered_nodes {
555 tracing::debug!(
556 "Processing ordered node: {}/{} (source: {:?})",
557 node.resource_type,
558 node.name,
559 node.source
560 );
561
562 for (key, dep) in &all_deps {
564 if key.0 == node.resource_type && key.1 == node.name && key.2 == node.source {
565 tracing::debug!(
566 " -> Found match in all_deps, adding to result with type {:?}",
567 node.resource_type
568 );
569 result.push((node.name.clone(), dep.clone(), node.resource_type));
570 added_keys.insert(key.clone());
571 break;
572 }
573 }
574 }
575
576 for (key, dep) in all_deps {
578 if !added_keys.contains(&key) && !dep.is_pattern() {
579 tracing::debug!(
580 "Adding non-graph dependency: {}/{} (source: {:?}) with type {:?}",
581 key.0,
582 key.1,
583 key.2,
584 key.0
585 );
586 result.push((key.1.clone(), dep.clone(), key.0));
587 }
588 }
589
590 tracing::debug!("Transitive resolution returning {} dependencies", result.len());
591
592 Ok(result)
593}
594
595pub fn group_key(source: &str, version: &str) -> String {
597 format!("{source}::{version}")
598}
599
600pub async fn resolve_with_services(
605 ctx: &mut TransitiveContext<'_>,
606 core: &super::ResolutionCore,
607 base_deps: &[(String, ResourceDependency, ResourceType)],
608 enable_transitive: bool,
609 prepared_versions: &HashMap<String, PreparedSourceVersion>,
610 pattern_alias_map: &mut HashMap<(ResourceType, String), String>,
611 services: &mut ResolutionServices<'_>,
612) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
613 ctx.dependency_map.clear();
615
616 if !enable_transitive {
617 return Ok(base_deps.to_vec());
618 }
619
620 let mut graph = DependencyGraph::new();
621 let mut all_deps: HashMap<DependencyKey, ResourceDependency> = HashMap::new();
622 let mut processed: HashSet<DependencyKey> = HashSet::new();
623 let mut queue: Vec<(String, ResourceDependency, Option<ResourceType>, String)> = Vec::new();
624
625 for (name, dep, resource_type) in base_deps {
627 let source = dep.get_source().map(std::string::ToString::to_string);
628 let tool = dep.get_tool().map(std::string::ToString::to_string);
629
630 let merged_variant_inputs =
633 super::lockfile_builder::build_merged_variant_inputs(ctx.base.manifest, dep);
634 let variant_hash = crate::utils::compute_variant_inputs_hash(&merged_variant_inputs)
635 .unwrap_or_else(|_| crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string());
636
637 tracing::debug!(
638 "[DEBUG] Adding base dep to queue: '{}' (type: {:?}, source: {:?}, tool: {:?}, is_local: {})",
639 name,
640 resource_type,
641 source,
642 tool,
643 dep.is_local()
644 );
645 queue.push((name.clone(), dep.clone(), Some(*resource_type), variant_hash.clone()));
647 all_deps.insert((*resource_type, name.clone(), source, tool, variant_hash), dep.clone());
648 }
649
650 while let Some((name, dep, resource_type, variant_hash)) = queue.pop() {
652 let source = dep.get_source().map(std::string::ToString::to_string);
653 let tool = dep.get_tool().map(std::string::ToString::to_string);
654
655 let resource_type =
656 resource_type.expect("resource_type should always be threaded through queue");
657 let key = (resource_type, name.clone(), source.clone(), tool.clone(), variant_hash.clone());
658
659 tracing::debug!(
660 "[TRANSITIVE] Processing: '{}' (type: {:?}, source: {:?})",
661 name,
662 resource_type,
663 source
664 );
665
666 if let Some(current_dep) = all_deps.get(&key) {
668 if current_dep.get_version() != dep.get_version() {
669 tracing::debug!("[TRANSITIVE] Skipped stale: '{}'", name);
670 continue;
671 }
672 }
673
674 if processed.contains(&key) {
675 tracing::debug!("[TRANSITIVE] Already processed: '{}'", name);
676 continue;
677 }
678
679 processed.insert(key.clone());
680
681 if dep.is_pattern() {
683 tracing::debug!("[TRANSITIVE] Expanding pattern: '{}'", name);
684 match services
685 .pattern_service
686 .expand_pattern(core, &dep, resource_type, services.version_service)
687 .await
688 {
689 Ok(concrete_deps) => {
690 for (concrete_name, concrete_dep) in concrete_deps {
691 pattern_alias_map
692 .insert((resource_type, concrete_name.clone()), name.clone());
693
694 let concrete_source =
695 concrete_dep.get_source().map(std::string::ToString::to_string);
696 let concrete_tool =
697 concrete_dep.get_tool().map(std::string::ToString::to_string);
698 let concrete_variant_hash = compute_dependency_variant_hash(&concrete_dep);
699 let concrete_key = (
700 resource_type,
701 concrete_name.clone(),
702 concrete_source,
703 concrete_tool,
704 concrete_variant_hash.clone(),
705 );
706
707 if let std::collections::hash_map::Entry::Vacant(e) =
708 all_deps.entry(concrete_key)
709 {
710 e.insert(concrete_dep.clone());
711 queue.push((
712 concrete_name,
713 concrete_dep,
714 Some(resource_type),
715 concrete_variant_hash,
716 ));
717 }
718 }
719 }
720 Err(e) => {
721 anyhow::bail!("Failed to expand pattern '{}': {}", dep.get_path(), e);
722 }
723 }
724 continue;
725 }
726
727 let content = ResourceFetchingService::fetch_content(core, &dep, services.version_service)
729 .await
730 .with_context(|| format!("Failed to fetch resource '{}' for transitive deps", name))?;
731
732 tracing::debug!("[TRANSITIVE] Fetched content for '{}' ({} bytes)", name, content.len());
733
734 let variant_inputs_value =
737 super::lockfile_builder::build_merged_variant_inputs(ctx.base.manifest, &dep);
738 let variant_inputs = Some(&variant_inputs_value);
739
740 let path = PathBuf::from(dep.get_path());
742 let metadata = MetadataExtractor::extract(
743 &path,
744 &content,
745 variant_inputs,
746 ctx.base.operation_context.map(|arc| arc.as_ref()),
747 )?;
748
749 tracing::debug!(
750 "[DEBUG] Extracted metadata for '{}': has_deps={}",
751 name,
752 metadata.get_dependencies().is_some()
753 );
754
755 if let Some(deps_map) = metadata.get_dependencies() {
757 tracing::debug!(
758 "[DEBUG] Found {} dependency type(s) for '{}': {:?}",
759 deps_map.len(),
760 name,
761 deps_map.keys().collect::<Vec<_>>()
762 );
763
764 for (dep_resource_type_str, dep_specs) in deps_map {
765 let dep_resource_type: ResourceType =
766 dep_resource_type_str.parse().unwrap_or(ResourceType::Snippet);
767
768 for dep_spec in dep_specs {
769 let (trans_dep, trans_name) = process_transitive_dependency_spec(
771 ctx,
772 core,
773 &dep,
774 dep_resource_type,
775 resource_type,
776 &name,
777 dep_spec,
778 services.version_service,
779 prepared_versions,
780 )
781 .await?;
782
783 let trans_source = trans_dep.get_source().map(std::string::ToString::to_string);
784 let trans_tool = trans_dep.get_tool().map(std::string::ToString::to_string);
785 let trans_variant_hash = compute_dependency_variant_hash(&trans_dep);
786
787 if let Some(custom_name) = &dep_spec.name {
789 let trans_key = (
790 dep_resource_type,
791 trans_name.clone(),
792 trans_source.clone(),
793 trans_tool.clone(),
794 trans_variant_hash.clone(),
795 );
796 ctx.transitive_custom_names.insert(trans_key, custom_name.clone());
797 tracing::debug!(
798 "Storing custom name '{}' for transitive dep '{}'",
799 custom_name,
800 trans_name
801 );
802 }
803
804 let from_node =
806 DependencyNode::with_source(resource_type, &name, source.clone());
807 let to_node = DependencyNode::with_source(
808 dep_resource_type,
809 &trans_name,
810 trans_source.clone(),
811 );
812 graph.add_dependency(from_node, to_node);
813
814 let from_key = (
816 resource_type,
817 name.clone(),
818 source.clone(),
819 tool.clone(),
820 variant_hash.clone(),
821 );
822 let dep_ref =
823 LockfileDependencyRef::local(dep_resource_type, trans_name.clone(), None)
824 .to_string();
825 tracing::debug!(
826 "[DEBUG] Adding to dependency_map: parent='{}' (type={:?}, source={:?}, tool={:?}, hash={}), child='{}' (type={:?})",
827 name,
828 resource_type,
829 source,
830 tool,
831 &variant_hash[..8],
832 dep_ref,
833 dep_resource_type
834 );
835 ctx.dependency_map.entry(from_key).or_default().push(dep_ref);
836
837 add_to_conflict_detector(ctx, &trans_name, &trans_dep, &name);
839
840 let trans_key = (
842 dep_resource_type,
843 trans_name.clone(),
844 trans_source.clone(),
845 trans_tool.clone(),
846 trans_variant_hash.clone(),
847 );
848
849 tracing::debug!(
850 "[TRANSITIVE] Found transitive dep '{}' (type: {:?}, tool: {:?}, parent: {})",
851 trans_name,
852 dep_resource_type,
853 trans_tool,
854 name
855 );
856
857 if let std::collections::hash_map::Entry::Vacant(e) = all_deps.entry(trans_key)
859 {
860 tracing::debug!(
862 "Adding transitive dep '{}' (parent: {})",
863 trans_name,
864 name
865 );
866 e.insert(trans_dep.clone());
867 queue.push((
868 trans_name,
869 trans_dep,
870 Some(dep_resource_type),
871 trans_variant_hash,
872 ));
873 } else {
874 tracing::debug!(
876 "[TRANSITIVE] Skipping duplicate transitive dep '{}' (already processed)",
877 trans_name
878 );
879 }
880 }
881 }
882 }
883 }
884
885 graph.detect_cycles()?;
887
888 let ordered_nodes = graph.topological_order()?;
890
891 build_ordered_result(all_deps, ordered_nodes)
893}