1use std::collections::HashSet;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23use std::sync::atomic::{AtomicUsize, Ordering};
24
25use anyhow::{Context, Result};
26use dashmap::DashMap;
27use futures::future::join_all;
28use tokio::sync::{Mutex, MutexGuard};
29
30use crate::core::ResourceType;
31use crate::lockfile::lockfile_dependency_ref::LockfileDependencyRef;
32use crate::manifest::{DetailedDependency, ResourceDependency};
33use crate::metadata::MetadataExtractor;
34use crate::utils;
35use crate::version::conflict::ConflictDetector;
36use crate::version::constraints::VersionConstraint;
37
38use super::dependency_graph::{DependencyGraph, DependencyNode};
39use super::pattern_expander::generate_dependency_name;
40use super::types::{DependencyKey, TransitiveContext, apply_manifest_override};
41use super::version_resolver::{PreparedSourceVersion, VersionResolutionService};
42use super::{PatternExpansionService, ResourceFetchingService, is_file_relative_path};
43
44use crate::constants::{batch_operation_timeout, default_lock_timeout};
45
46async fn acquire_mutex_with_timeout<'a, T>(
52 mutex: &'a Mutex<T>,
53 name: &str,
54) -> Result<MutexGuard<'a, T>> {
55 let timeout = default_lock_timeout();
56 match tokio::time::timeout(timeout, mutex.lock()).await {
57 Ok(guard) => Ok(guard),
58 Err(_) => {
59 eprintln!("[DEADLOCK] Timeout waiting for mutex '{}' after {:?}", name, timeout);
60 anyhow::bail!(
61 "Timeout waiting for Mutex '{}' after {:?} - possible deadlock",
62 name,
63 timeout
64 )
65 }
66 }
67}
68
69fn is_semver_version(version: Option<&str>) -> bool {
75 match version {
76 Some(v) => VersionConstraint::parse(v).is_ok_and(|c| c.is_semver()),
77 None => false,
78 }
79}
80
81fn should_replace_existing(existing: &ResourceDependency, new: &ResourceDependency) -> bool {
89 let existing_is_semver = is_semver_version(existing.get_version());
90 let new_is_semver = is_semver_version(new.get_version());
91
92 if new_is_semver && !existing_is_semver {
96 tracing::debug!(
97 "Preferring semver '{}' over git ref '{}'",
98 new.get_version().unwrap_or("none"),
99 existing.get_version().unwrap_or("none")
100 );
101 return true;
102 }
103
104 false
105}
106
107pub struct ResolutionServices<'a> {
109 pub version_service: &'a VersionResolutionService,
111 pub pattern_service: &'a PatternExpansionService,
113}
114
115pub struct TransitiveResolutionParams<'a> {
117 pub ctx: &'a mut TransitiveContext<'a>,
119 pub core: &'a super::ResolutionCore,
121 pub base_deps: &'a [(String, ResourceDependency, ResourceType)],
123 pub enable_transitive: bool,
125 pub prepared_versions: &'a Arc<DashMap<String, PreparedSourceVersion>>,
127 pub pattern_alias_map: &'a Arc<DashMap<(ResourceType, String), String>>,
129 pub services: &'a ResolutionServices<'a>,
131 pub progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
133}
134
135struct TransitiveDepProcessingParams<'a> {
139 ctx: &'a TransitiveContext<'a>,
141 core: &'a super::ResolutionCore,
143 parent_dep: &'a ResourceDependency,
145 dep_resource_type: ResourceType,
147 parent_resource_type: ResourceType,
149 parent_name: &'a str,
151 dep_spec: &'a crate::manifest::DependencySpec,
153 version_service: &'a VersionResolutionService,
155 prepared_versions: &'a Arc<DashMap<String, PreparedSourceVersion>>,
157}
158
159struct TransitiveProcessingContext<'a> {
169 input: TransitiveInput,
171
172 shared: TransitiveSharedState<'a>,
174
175 resolution: TransitiveResolutionContext<'a>,
177
178 progress: Option<Arc<utils::MultiPhaseProgress>>,
180}
181
182#[derive(Debug, Clone)]
187struct TransitiveInput {
188 name: String,
189 dep: ResourceDependency,
190 resource_type: ResourceType,
191 variant_hash: String,
192}
193
194type QueueEntry = (String, ResourceDependency, Option<ResourceType>, String);
198
199struct TransitiveSharedState<'a> {
208 graph: Arc<tokio::sync::Mutex<DependencyGraph>>,
209 all_deps: Arc<DashMap<DependencyKey, ResourceDependency>>,
210 processed: Arc<DashMap<DependencyKey, ()>>,
211 queue: Arc<tokio::sync::Mutex<Vec<QueueEntry>>>,
212 queue_len: Arc<AtomicUsize>,
215 pattern_alias_map: Arc<DashMap<(ResourceType, String), String>>,
216 completed_counter: Arc<AtomicUsize>,
217 dependency_map: &'a Arc<DashMap<DependencyKey, Vec<String>>>,
218 custom_names: &'a Arc<DashMap<DependencyKey, String>>,
219 prepared_versions: &'a Arc<DashMap<String, PreparedSourceVersion>>,
220}
221
222struct TransitiveResolutionContext<'a> {
228 ctx_base: &'a super::types::ResolutionContext<'a>,
229 manifest_overrides: &'a super::types::ManifestOverrideIndex,
230 core: &'a super::ResolutionCore,
231 services: &'a ResolutionServices<'a>,
232}
233
234async fn process_transitive_dependency_spec(
236 params: TransitiveDepProcessingParams<'_>,
237) -> Result<(ResourceDependency, String)> {
238 let parent_file_path = ResourceFetchingService::get_canonical_path(
240 params.core,
241 params.parent_dep,
242 params.version_service,
243 )
244 .await
245 .with_context(|| {
246 format!("Failed to get parent path for transitive dependencies of '{}'", params.parent_name)
247 })?;
248
249 let trans_canonical =
251 resolve_transitive_path(&parent_file_path, ¶ms.dep_spec.path, params.parent_name)?;
252
253 let trans_dep = create_transitive_dependency(
255 params.ctx,
256 params.parent_dep,
257 params.dep_resource_type,
258 params.parent_resource_type,
259 params.parent_name,
260 params.dep_spec,
261 &parent_file_path,
262 &trans_canonical,
263 params.prepared_versions,
264 )
265 .await?;
266
267 let trans_name = if trans_dep.get_source().is_none() {
269 let manifest_dir = params
273 .ctx
274 .base
275 .manifest
276 .manifest_dir
277 .as_ref()
278 .ok_or_else(|| anyhow::anyhow!("Manifest directory not available"))?;
279
280 let source_context = crate::resolver::source_context::SourceContext::local(manifest_dir);
281 generate_dependency_name(trans_dep.get_path(), &source_context)
282 } else {
283 let source_name = trans_dep
285 .get_source()
286 .ok_or_else(|| anyhow::anyhow!("Git dependency missing source name"))?;
287 let source_context = crate::resolver::source_context::SourceContext::remote(source_name);
288 generate_dependency_name(trans_dep.get_path(), &source_context)
289 };
290
291 Ok((trans_dep, trans_name))
292}
293
294fn resolve_transitive_path(
296 parent_file_path: &Path,
297 dep_path: &str,
298 parent_name: &str,
299) -> Result<PathBuf> {
300 let is_pattern = dep_path.contains('*') || dep_path.contains('?') || dep_path.contains('[');
302
303 if is_pattern {
304 let parent_dir = parent_file_path.parent().ok_or_else(|| {
306 anyhow::anyhow!(
307 "Failed to resolve transitive dependency '{}' for '{}': parent file has no directory",
308 dep_path,
309 parent_name
310 )
311 })?;
312 let resolved = parent_dir.join(dep_path);
313
314 let mut result = PathBuf::new();
316 for component in resolved.components() {
317 match component {
318 std::path::Component::RootDir => result.push(component),
319 std::path::Component::ParentDir => {
320 result.pop();
321 }
322 std::path::Component::CurDir => {}
323 _ => result.push(component),
324 }
325 }
326 Ok(result)
327 } else if is_file_relative_path(dep_path) || !dep_path.contains('/') {
328 let parent_dir = parent_file_path.parent().ok_or_else(|| {
331 anyhow::anyhow!(
332 "Failed to resolve transitive dependency '{}' for '{}': parent file has no directory",
333 dep_path,
334 parent_name
335 )
336 })?;
337
338 let resolved = parent_dir.join(dep_path);
339 resolved.canonicalize().map_err(|e| {
340 let file_error = crate::core::file_error::FileOperationError::new(
342 crate::core::file_error::FileOperationContext::new(
343 crate::core::file_error::FileOperation::Canonicalize,
344 &resolved,
345 format!("resolving transitive dependency '{}' for '{}'", dep_path, parent_name),
346 "transitive_resolver::resolve_transitive_path",
347 ),
348 e,
349 );
350 anyhow::Error::from(file_error)
351 })
352 } else {
353 resolve_repo_relative_path(parent_file_path, dep_path, parent_name)
355 }
356}
357
358fn resolve_repo_relative_path(
360 parent_file_path: &Path,
361 dep_path: &str,
362 parent_name: &str,
363) -> Result<PathBuf> {
364 let repo_root = parent_file_path
366 .ancestors()
367 .find(|p| {
368 let git_path = p.join(".git");
371 git_path.is_file()
372 })
373 .or_else(|| parent_file_path.ancestors().nth(2)) .ok_or_else(|| {
375 anyhow::anyhow!(
376 "Failed to find repository root for transitive dependency '{}'",
377 dep_path
378 )
379 })?;
380
381 let full_path = repo_root.join(dep_path);
382 full_path.canonicalize().with_context(|| {
383 format!(
384 "Failed to resolve repo-relative transitive dependency '{}' for '{}': {} (repo root: {})",
385 dep_path,
386 parent_name,
387 full_path.display(),
388 repo_root.display()
389 )
390 })
391}
392
393#[allow(clippy::too_many_arguments)]
395async fn create_transitive_dependency(
396 ctx: &TransitiveContext<'_>,
397 parent_dep: &ResourceDependency,
398 dep_resource_type: ResourceType,
399 parent_resource_type: ResourceType,
400 _parent_name: &str,
401 dep_spec: &crate::manifest::DependencySpec,
402 parent_file_path: &Path,
403 trans_canonical: &Path,
404 prepared_versions: &Arc<DashMap<String, PreparedSourceVersion>>,
405) -> Result<ResourceDependency> {
406 use super::types::{OverrideKey, normalize_lookup_path};
407
408 let mut dep = if parent_dep.get_source().is_none() {
410 create_path_only_transitive_dep(
411 ctx,
412 parent_dep,
413 dep_resource_type,
414 parent_resource_type,
415 dep_spec,
416 trans_canonical,
417 )?
418 } else {
419 create_git_backed_transitive_dep(
420 ctx,
421 parent_dep,
422 dep_resource_type,
423 parent_resource_type,
424 dep_spec,
425 parent_file_path,
426 trans_canonical,
427 prepared_versions,
428 )
429 .await?
430 };
431
432 let normalized_path = normalize_lookup_path(dep.get_path());
434 let source = dep.get_source().map(std::string::ToString::to_string);
435
436 let tool = dep
438 .get_tool()
439 .map(str::to_string)
440 .unwrap_or_else(|| ctx.base.manifest.get_default_tool(dep_resource_type));
441
442 let variant_hash =
443 super::lockfile_builder::compute_merged_variant_hash(ctx.base.manifest, &dep);
444
445 let override_key = OverrideKey {
446 resource_type: dep_resource_type,
447 normalized_path: normalized_path.clone(),
448 source,
449 tool,
450 variant_hash,
451 };
452
453 if let Some(override_info) = ctx.manifest_overrides.get(&override_key) {
455 apply_manifest_override(&mut dep, override_info, &normalized_path);
456 }
457
458 Ok(dep)
459}
460
461fn create_path_only_transitive_dep(
463 ctx: &TransitiveContext<'_>,
464 parent_dep: &ResourceDependency,
465 dep_resource_type: ResourceType,
466 parent_resource_type: ResourceType,
467 dep_spec: &crate::manifest::DependencySpec,
468 trans_canonical: &Path,
469) -> Result<ResourceDependency> {
470 let manifest_dir = ctx.base.manifest.manifest_dir.as_ref().ok_or_else(|| {
471 anyhow::anyhow!("Manifest directory not available for path-only transitive dep")
472 })?;
473
474 let dep_path_str = match manifest_dir.canonicalize() {
476 Ok(canonical_manifest) => {
477 utils::compute_relative_path(&canonical_manifest, trans_canonical)
478 }
479 Err(e) => {
480 eprintln!(
481 "Warning: Could not canonicalize manifest directory {}: {}. Using non-canonical path.",
482 manifest_dir.display(),
483 e
484 );
485 utils::compute_relative_path(manifest_dir, trans_canonical)
486 }
487 };
488
489 let trans_tool = determine_transitive_tool(
491 ctx,
492 parent_dep,
493 dep_spec,
494 parent_resource_type,
495 dep_resource_type,
496 );
497
498 Ok(ResourceDependency::Detailed(Box::new(DetailedDependency {
499 source: None,
500 path: utils::normalize_path_for_storage(dep_path_str),
501 version: None,
502 branch: None,
503 rev: None,
504 command: None,
505 args: None,
506 target: None,
507 filename: None,
508 dependencies: None,
509 tool: trans_tool,
510 flatten: None,
511 install: dep_spec.install.or(Some(true)),
512 template_vars: Some(super::lockfile_builder::build_merged_variant_inputs(
513 ctx.base.manifest,
514 parent_dep,
515 )),
516 })))
517}
518
519#[allow(clippy::too_many_arguments)]
521async fn create_git_backed_transitive_dep(
522 ctx: &TransitiveContext<'_>,
523 parent_dep: &ResourceDependency,
524 dep_resource_type: ResourceType,
525 parent_resource_type: ResourceType,
526 dep_spec: &crate::manifest::DependencySpec,
527 parent_file_path: &Path,
528 trans_canonical: &Path,
529 _prepared_versions: &Arc<DashMap<String, PreparedSourceVersion>>,
530) -> Result<ResourceDependency> {
531 let source_name = parent_dep
532 .get_source()
533 .ok_or_else(|| anyhow::anyhow!("Expected source for Git-backed dependency"))?;
534 let source_url = ctx
535 .base
536 .source_manager
537 .get_source_url(source_name)
538 .ok_or_else(|| anyhow::anyhow!("Source '{source_name}' not found"))?;
539
540 let repo_relative = if utils::is_local_path(&source_url) {
542 strip_local_source_prefix(&source_url, trans_canonical)?
543 } else {
544 strip_git_worktree_prefix_from_parent(parent_file_path, trans_canonical)?
546 };
547
548 let trans_tool = determine_transitive_tool(
550 ctx,
551 parent_dep,
552 dep_spec,
553 parent_resource_type,
554 dep_resource_type,
555 );
556
557 Ok(ResourceDependency::Detailed(Box::new(DetailedDependency {
558 source: Some(source_name.to_string()),
559 path: utils::normalize_path_for_storage(repo_relative.to_string_lossy().to_string()),
560 version: dep_spec
561 .version
562 .clone()
563 .or_else(|| parent_dep.get_version().map(|v| v.to_string())),
564 branch: None,
565 rev: None,
566 command: None,
567 args: None,
568 target: None,
569 filename: None,
570 dependencies: None,
571 tool: trans_tool,
572 flatten: None,
573 install: dep_spec.install.or(Some(true)),
574 template_vars: Some(super::lockfile_builder::build_merged_variant_inputs(
575 ctx.base.manifest,
576 parent_dep,
577 )),
578 })))
579}
580
581fn strip_local_source_prefix(source_url: &str, trans_canonical: &Path) -> Result<PathBuf> {
583 let source_url_path = PathBuf::from(source_url);
584 let source_path = source_url_path.canonicalize().map_err(|e| {
585 let file_error = crate::core::file_error::FileOperationError::new(
586 crate::core::file_error::FileOperationContext::new(
587 crate::core::file_error::FileOperation::Canonicalize,
588 &source_url_path,
589 "canonicalizing local source path for transitive dependency".to_string(),
590 "transitive_resolver::strip_local_source_prefix",
591 ),
592 e,
593 );
594 anyhow::Error::from(file_error)
595 })?;
596
597 let trans_str = trans_canonical.to_string_lossy();
599 let is_pattern = trans_str.contains('*') || trans_str.contains('?') || trans_str.contains('[');
600
601 if is_pattern {
602 let parent_dir = trans_canonical.parent().ok_or_else(|| {
604 anyhow::anyhow!("Pattern path has no parent directory: {}", trans_canonical.display())
605 })?;
606 let filename = trans_canonical.file_name().ok_or_else(|| {
607 anyhow::anyhow!("Pattern path has no filename: {}", trans_canonical.display())
608 })?;
609
610 let canonical_dir = parent_dir.canonicalize().map_err(|e| {
612 let file_error = crate::core::file_error::FileOperationError::new(
613 crate::core::file_error::FileOperationContext::new(
614 crate::core::file_error::FileOperation::Canonicalize,
615 parent_dir,
616 "canonicalizing pattern directory for local source".to_string(),
617 "transitive_resolver::strip_local_source_prefix",
618 ),
619 e,
620 );
621 anyhow::Error::from(file_error)
622 })?;
623
624 let canonical_pattern = canonical_dir.join(filename);
626
627 canonical_pattern
629 .strip_prefix(&source_path)
630 .with_context(|| {
631 format!(
632 "Transitive pattern dep outside parent's source: {} not under {}",
633 canonical_pattern.display(),
634 source_path.display()
635 )
636 })
637 .map(|p| p.to_path_buf())
638 } else {
639 trans_canonical
640 .strip_prefix(&source_path)
641 .with_context(|| {
642 format!(
643 "Transitive dep resolved outside parent's source directory: {} not under {}",
644 trans_canonical.display(),
645 source_path.display()
646 )
647 })
648 .map(|p| p.to_path_buf())
649 }
650}
651
652fn strip_git_worktree_prefix_from_parent(
655 parent_file_path: &Path,
656 trans_canonical: &Path,
657) -> Result<PathBuf> {
658 let worktree_root = parent_file_path
662 .ancestors()
663 .find(|p| {
664 let git_path = p.join(".git");
665 git_path.is_file()
666 })
667 .ok_or_else(|| {
668 anyhow::anyhow!(
669 "Failed to find worktree root from parent file: {}",
670 parent_file_path.display()
671 )
672 })?;
673
674 let canonical_worktree = worktree_root.canonicalize().map_err(|e| {
676 let file_error = crate::core::file_error::FileOperationError::new(
677 crate::core::file_error::FileOperationContext::new(
678 crate::core::file_error::FileOperation::Canonicalize,
679 worktree_root,
680 "canonicalizing worktree root for transitive dependency".to_string(),
681 "transitive_resolver::strip_git_worktree_prefix_from_parent",
682 ),
683 e,
684 );
685 anyhow::Error::from(file_error)
686 })?;
687
688 let trans_str = trans_canonical.to_string_lossy();
690 let is_pattern = trans_str.contains('*') || trans_str.contains('?') || trans_str.contains('[');
691
692 if is_pattern {
693 let parent_dir = trans_canonical.parent().ok_or_else(|| {
695 anyhow::anyhow!("Pattern path has no parent directory: {}", trans_canonical.display())
696 })?;
697 let filename = trans_canonical.file_name().ok_or_else(|| {
698 anyhow::anyhow!("Pattern path has no filename: {}", trans_canonical.display())
699 })?;
700
701 let canonical_dir = parent_dir.canonicalize().map_err(|e| {
703 let file_error = crate::core::file_error::FileOperationError::new(
704 crate::core::file_error::FileOperationContext::new(
705 crate::core::file_error::FileOperation::Canonicalize,
706 parent_dir,
707 "canonicalizing pattern directory for Git worktree".to_string(),
708 "transitive_resolver::strip_git_worktree_prefix_from_parent",
709 ),
710 e,
711 );
712 anyhow::Error::from(file_error)
713 })?;
714
715 let canonical_pattern = canonical_dir.join(filename);
717
718 canonical_pattern
720 .strip_prefix(&canonical_worktree)
721 .with_context(|| {
722 format!(
723 "Transitive pattern dep outside parent's worktree: {} not under {}",
724 canonical_pattern.display(),
725 canonical_worktree.display()
726 )
727 })
728 .map(|p| p.to_path_buf())
729 } else {
730 trans_canonical
731 .strip_prefix(&canonical_worktree)
732 .with_context(|| {
733 format!(
734 "Transitive dep outside parent's worktree: {} not under {}",
735 trans_canonical.display(),
736 canonical_worktree.display()
737 )
738 })
739 .map(|p| p.to_path_buf())
740 }
741}
742
743fn determine_transitive_tool(
745 ctx: &TransitiveContext<'_>,
746 parent_dep: &ResourceDependency,
747 dep_spec: &crate::manifest::DependencySpec,
748 parent_resource_type: ResourceType,
749 dep_resource_type: ResourceType,
750) -> Option<String> {
751 if let Some(explicit_tool) = &dep_spec.tool {
752 Some(explicit_tool.clone())
753 } else {
754 let parent_tool = parent_dep
755 .get_tool()
756 .map(str::to_string)
757 .unwrap_or_else(|| ctx.base.manifest.get_default_tool(parent_resource_type));
758 if ctx.base.manifest.is_resource_supported(&parent_tool, dep_resource_type) {
759 Some(parent_tool)
760 } else {
761 Some(ctx.base.manifest.get_default_tool(dep_resource_type))
762 }
763 }
764}
765
766fn build_ordered_result(
768 all_deps: Arc<DashMap<DependencyKey, ResourceDependency>>,
769 ordered_nodes: Vec<DependencyNode>,
770) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
771 let mut result = Vec::new();
772 let mut added_keys = HashSet::new();
773
774 tracing::debug!(
775 "Transitive resolution - topological order has {} nodes, all_deps has {} entries",
776 ordered_nodes.len(),
777 all_deps.len()
778 );
779
780 for node in ordered_nodes {
781 tracing::debug!(
782 "Processing ordered node: {}/{} (source: {:?})",
783 node.resource_type,
784 node.name,
785 node.source
786 );
787
788 for entry in all_deps.iter() {
790 let (key, dep) = (entry.key(), entry.value());
791 if key.0 == node.resource_type && key.1 == node.name && key.2 == node.source {
792 tracing::debug!(
793 " -> Found match in all_deps, adding to result with type {:?}",
794 node.resource_type
795 );
796 result.push((node.name.clone(), dep.clone(), node.resource_type));
797 added_keys.insert(key.clone());
798 break;
799 }
800 }
801 }
802
803 for entry in all_deps.iter() {
805 let (key, dep) = (entry.key(), entry.value());
806 if !added_keys.contains(key) && !dep.is_pattern() {
807 tracing::debug!(
808 "Adding non-graph dependency: {}/{} (source: {:?}) with type {:?}",
809 key.0,
810 key.1,
811 key.2,
812 key.0
813 );
814 result.push((key.1.clone(), dep.clone(), key.0));
815 }
816 }
817
818 tracing::debug!("Transitive resolution returning {} dependencies", result.len());
819
820 Ok(result)
821}
822
823pub fn group_key(source: &str, version: &str) -> String {
825 format!("{source}::{version}")
826}
827
828async fn process_single_transitive_dependency<'a>(
833 ctx: TransitiveProcessingContext<'a>,
834) -> Result<()> {
835 let source = ctx.input.dep.get_source().map(std::string::ToString::to_string);
836 let tool =
839 Some(ctx.input.dep.get_tool().map(std::string::ToString::to_string).unwrap_or_else(|| {
840 ctx.resolution.ctx_base.manifest.get_default_tool(ctx.input.resource_type)
841 }));
842
843 let key = (
844 ctx.input.resource_type,
845 ctx.input.name.clone(),
846 source.clone(),
847 tool.clone(),
848 ctx.input.variant_hash.clone(),
849 );
850
851 let display_name = if source.is_some() {
853 if let Some(version) = ctx.input.dep.get_version() {
854 format!("{}@{}", ctx.input.name, version)
855 } else {
856 format!("{}@HEAD", ctx.input.name)
857 }
858 } else {
859 ctx.input.name.clone()
860 };
861 let progress_key = format!("{}:{}", ctx.input.resource_type, &display_name);
862
863 if let Some(ref pm) = ctx.progress {
865 pm.mark_item_active(&display_name, &progress_key);
866 }
867
868 tracing::debug!(
869 "[TRANSITIVE] Processing: '{}' (type: {:?}, source: {:?})",
870 ctx.input.name,
871 ctx.input.resource_type,
872 source
873 );
874
875 let is_stale = ctx
880 .shared
881 .all_deps
882 .get(&key)
883 .map(|current_dep| current_dep.get_version() != ctx.input.dep.get_version())
884 .unwrap_or(false);
885
886 if is_stale {
887 tracing::debug!("[TRANSITIVE] Skipped stale: '{}'", ctx.input.name);
888 if let Some(ref pm) = ctx.progress {
890 let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
891 let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
892 pm.mark_item_complete(
893 &progress_key,
894 Some(&display_name),
895 completed,
896 total,
897 "Scanning dependencies",
898 );
899 }
900 return Ok(());
901 }
902
903 if ctx.shared.processed.contains_key(&key) {
904 tracing::debug!("[TRANSITIVE] Already processed: '{}'", ctx.input.name);
905 if let Some(ref pm) = ctx.progress {
906 let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
907 let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
908 pm.mark_item_complete(
909 &progress_key,
910 Some(&display_name),
911 completed,
912 total,
913 "Scanning dependencies",
914 );
915 }
916 return Ok(());
917 }
918
919 ctx.shared.processed.insert(key.clone(), ());
920
921 if ctx.input.dep.is_pattern() {
923 tracing::debug!("[TRANSITIVE] Expanding pattern: '{}'", ctx.input.name);
924 match ctx
925 .resolution
926 .services
927 .pattern_service
928 .expand_pattern(
929 ctx.resolution.core,
930 &ctx.input.dep,
931 ctx.input.resource_type,
932 ctx.shared.prepared_versions.as_ref(),
933 )
934 .await
935 {
936 Ok(concrete_deps) => {
937 let mut items_to_queue = Vec::new();
941
942 for (concrete_name, concrete_dep) in concrete_deps {
943 ctx.shared.pattern_alias_map.insert(
944 (ctx.input.resource_type, concrete_name.clone()),
945 ctx.input.name.clone(),
946 );
947
948 let concrete_source =
949 concrete_dep.get_source().map(std::string::ToString::to_string);
950 let concrete_tool =
951 concrete_dep.get_tool().map(std::string::ToString::to_string);
952 let concrete_variant_hash =
953 super::lockfile_builder::compute_merged_variant_hash(
954 ctx.resolution.ctx_base.manifest,
955 &concrete_dep,
956 );
957 let concrete_key = (
958 ctx.input.resource_type,
959 concrete_name.clone(),
960 concrete_source,
961 concrete_tool,
962 concrete_variant_hash.clone(),
963 );
964
965 match ctx.shared.all_deps.entry(concrete_key) {
967 dashmap::mapref::entry::Entry::Vacant(e) => {
968 e.insert(concrete_dep.clone());
969 items_to_queue.push((
971 concrete_name,
972 concrete_dep,
973 Some(ctx.input.resource_type),
974 concrete_variant_hash,
975 ));
976 }
977 dashmap::mapref::entry::Entry::Occupied(mut e) => {
978 let existing = e.get();
980 if should_replace_existing(existing, &concrete_dep) {
981 tracing::debug!(
982 "[PATTERN] Replacing existing dep '{}' with semver version",
983 concrete_name
984 );
985 e.insert(concrete_dep.clone());
986 items_to_queue.push((
987 concrete_name,
988 concrete_dep,
989 Some(ctx.input.resource_type),
990 concrete_variant_hash,
991 ));
992 }
993 }
994 }
995 }
997
998 if !items_to_queue.is_empty() {
1000 let items_count = items_to_queue.len();
1001 let mut queue =
1002 acquire_mutex_with_timeout(&ctx.shared.queue, "transitive_queue").await?;
1003 queue.extend(items_to_queue);
1004 ctx.shared.queue_len.fetch_add(items_count, Ordering::SeqCst);
1006 }
1007 }
1008 Err(e) => {
1009 anyhow::bail!("Failed to expand pattern '{}': {}", ctx.input.dep.get_path(), e);
1010 }
1011 }
1012 if let Some(ref pm) = ctx.progress {
1014 let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
1015 let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
1016 pm.mark_item_complete(
1017 &progress_key,
1018 Some(&display_name),
1019 completed,
1020 total,
1021 "Scanning dependencies",
1022 );
1023 }
1024 return Ok(());
1025 }
1026
1027 let content = if ctx.input.resource_type == ResourceType::Skill {
1030 let skill_md_dep = create_skill_md_dependency(&ctx.input.dep);
1032 ResourceFetchingService::fetch_content(
1033 ctx.resolution.core,
1034 &skill_md_dep,
1035 ctx.resolution.services.version_service,
1036 )
1037 .await
1038 .with_context(|| {
1039 format!(
1040 "Failed to fetch SKILL.md for skill '{}' ({})",
1041 ctx.input.name,
1042 ctx.input.dep.get_path()
1043 )
1044 })?
1045 } else {
1046 ResourceFetchingService::fetch_content(
1047 ctx.resolution.core,
1048 &ctx.input.dep,
1049 ctx.resolution.services.version_service,
1050 )
1051 .await
1052 .with_context(|| {
1053 format!(
1054 "Failed to fetch resource '{}' ({}) for transitive deps",
1055 ctx.input.name,
1056 ctx.input.dep.get_path()
1057 )
1058 })?
1059 };
1060
1061 tracing::debug!(
1066 "[TRANSITIVE] Fetched content for '{}' ({} bytes)",
1067 ctx.input.name,
1068 content.len()
1069 );
1070
1071 let variant_inputs_value = super::lockfile_builder::build_merged_variant_inputs(
1074 ctx.resolution.ctx_base.manifest,
1075 &ctx.input.dep,
1076 );
1077 let variant_inputs = Some(&variant_inputs_value);
1078
1079 let path = if ctx.input.resource_type == ResourceType::Skill {
1082 PathBuf::from(format!("{}/SKILL.md", ctx.input.dep.get_path().trim_end_matches('/')))
1083 } else {
1084 PathBuf::from(ctx.input.dep.get_path())
1085 };
1086 let metadata = MetadataExtractor::extract(
1087 &path,
1088 &content,
1089 variant_inputs,
1090 ctx.resolution.ctx_base.operation_context.map(|arc| arc.as_ref()),
1091 )?;
1092
1093 tracing::debug!(
1094 "[DEBUG] Extracted metadata for '{}': has_deps={}",
1095 ctx.input.name,
1096 metadata.get_dependencies().is_some()
1097 );
1098
1099 if let Some(deps_map) = metadata.get_dependencies() {
1101 tracing::debug!(
1102 "[DEBUG] Found {} dependency type(s) for '{}': {:?}",
1103 deps_map.len(),
1104 ctx.input.name,
1105 deps_map.keys().collect::<Vec<_>>()
1106 );
1107
1108 let mut items_to_queue = Vec::new();
1112
1113 let mut graph_edges: Vec<(DependencyNode, DependencyNode)> = Vec::new();
1117
1118 let declared_count = metadata.dependency_count();
1120 let declared_deps: Vec<(String, String)> = deps_map
1121 .iter()
1122 .flat_map(|(rtype, specs)| specs.iter().map(move |s| (rtype.clone(), s.path.clone())))
1123 .collect();
1124
1125 for (dep_resource_type_str, dep_specs) in deps_map {
1126 let dep_resource_type: ResourceType =
1127 dep_resource_type_str.parse().unwrap_or(ResourceType::Snippet);
1128
1129 for dep_spec in dep_specs {
1130 let mut dummy_conflict_detector = ConflictDetector::new();
1133 let temp_ctx = super::types::TransitiveContext {
1134 base: *ctx.resolution.ctx_base,
1135 dependency_map: ctx.shared.dependency_map,
1136 transitive_custom_names: ctx.shared.custom_names,
1137 conflict_detector: &mut dummy_conflict_detector,
1138 manifest_overrides: ctx.resolution.manifest_overrides,
1139 };
1140
1141 let (trans_dep, trans_name) =
1143 process_transitive_dependency_spec(TransitiveDepProcessingParams {
1144 ctx: &temp_ctx,
1145 core: ctx.resolution.core,
1146 parent_dep: &ctx.input.dep,
1147 dep_resource_type,
1148 parent_resource_type: ctx.input.resource_type,
1149 parent_name: &ctx.input.name,
1150 dep_spec,
1151 version_service: ctx.resolution.services.version_service,
1152 prepared_versions: ctx.shared.prepared_versions,
1153 })
1154 .await?;
1155
1156 let trans_source = trans_dep.get_source().map(std::string::ToString::to_string);
1157 let trans_tool = trans_dep.get_tool().map(std::string::ToString::to_string);
1158 let trans_variant_hash = super::lockfile_builder::compute_merged_variant_hash(
1159 ctx.resolution.ctx_base.manifest,
1160 &trans_dep,
1161 );
1162
1163 if let Some(custom_name) = &dep_spec.name {
1165 let trans_key = (
1166 dep_resource_type,
1167 trans_name.clone(),
1168 trans_source.clone(),
1169 trans_tool.clone(),
1170 trans_variant_hash.clone(),
1171 );
1172 ctx.shared.custom_names.insert(trans_key, custom_name.clone());
1173 tracing::debug!(
1174 "Storing custom name '{}' for transitive dep '{}'",
1175 custom_name,
1176 trans_name
1177 );
1178 }
1179
1180 let from_node = DependencyNode::with_source(
1182 ctx.input.resource_type,
1183 &ctx.input.name,
1184 source.clone(),
1185 );
1186 let to_node = DependencyNode::with_source(
1187 dep_resource_type,
1188 &trans_name,
1189 trans_source.clone(),
1190 );
1191 graph_edges.push((from_node, to_node));
1192
1193 let from_key = (
1195 ctx.input.resource_type,
1196 ctx.input.name.clone(),
1197 source.clone(),
1198 tool.clone(),
1199 ctx.input.variant_hash.clone(),
1200 );
1201 let dep_ref =
1202 LockfileDependencyRef::local(dep_resource_type, trans_name.clone(), None)
1203 .to_string();
1204 tracing::debug!(
1205 "[DEBUG] Adding to dependency_map: parent='{}' (type={:?}, source={:?}, tool={:?}, hash={}), child='{}' (type={:?})",
1206 ctx.input.name,
1207 ctx.input.resource_type,
1208 source,
1209 tool,
1210 &ctx.input.variant_hash[..8],
1211 dep_ref,
1212 dep_resource_type
1213 );
1214 ctx.shared.dependency_map.entry(from_key).or_default().push(dep_ref);
1215
1216 let trans_key = (
1221 dep_resource_type,
1222 trans_name.clone(),
1223 trans_source.clone(),
1224 trans_tool.clone(),
1225 trans_variant_hash.clone(),
1226 );
1227
1228 tracing::debug!(
1229 "[TRANSITIVE] Found transitive dep '{}' (type: {:?}, tool: {:?}, parent: {})",
1230 trans_name,
1231 dep_resource_type,
1232 trans_tool,
1233 ctx.input.name
1234 );
1235
1236 match ctx.shared.all_deps.entry(trans_key) {
1239 dashmap::mapref::entry::Entry::Vacant(e) => {
1240 tracing::debug!(
1242 "Adding transitive dep '{}' (parent: {})",
1243 trans_name,
1244 ctx.input.name
1245 );
1246 e.insert(trans_dep.clone());
1247 items_to_queue.push((
1249 trans_name,
1250 trans_dep,
1251 Some(dep_resource_type),
1252 trans_variant_hash,
1253 ));
1254 }
1255 dashmap::mapref::entry::Entry::Occupied(mut e) => {
1256 let existing = e.get();
1259 if should_replace_existing(existing, &trans_dep) {
1260 tracing::debug!(
1261 "[TRANSITIVE] Replacing existing dep '{}' (version: {:?}) with semver version {:?}",
1262 trans_name,
1263 existing.get_version(),
1264 trans_dep.get_version()
1265 );
1266 e.insert(trans_dep.clone());
1267 items_to_queue.push((
1269 trans_name,
1270 trans_dep,
1271 Some(dep_resource_type),
1272 trans_variant_hash,
1273 ));
1274 } else {
1275 tracing::debug!(
1276 "[TRANSITIVE] Keeping existing dep '{}' (version: {:?} vs new {:?})",
1277 trans_name,
1278 existing.get_version(),
1279 trans_dep.get_version()
1280 );
1281 }
1282 }
1283 }
1284 }
1286 }
1287
1288 let resolved_count = graph_edges.len();
1290
1291 if !graph_edges.is_empty() {
1293 let mut graph =
1294 acquire_mutex_with_timeout(&ctx.shared.graph, "dependency_graph").await?;
1295 for (from_node, to_node) in graph_edges {
1296 graph.add_dependency(from_node, to_node);
1297 }
1298 }
1299
1300 if !items_to_queue.is_empty() {
1302 let items_count = items_to_queue.len();
1303 let mut queue =
1304 acquire_mutex_with_timeout(&ctx.shared.queue, "transitive_queue").await?;
1305 queue.extend(items_to_queue);
1306 ctx.shared.queue_len.fetch_add(items_count, Ordering::SeqCst);
1308 }
1309
1310 if resolved_count < declared_count {
1312 return Err(crate::core::AgpmError::DependencyResolutionMismatch {
1313 resource: ctx.input.name.clone(),
1314 declared_count,
1315 resolved_count,
1316 declared_deps,
1317 }
1318 .into());
1319 }
1320 }
1321
1322 if let Some(ref pm) = ctx.progress {
1324 let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
1325 let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
1326 pm.mark_item_complete(
1327 &progress_key,
1328 Some(&display_name),
1329 completed,
1330 total,
1331 "Scanning dependencies",
1332 );
1333 }
1334
1335 Ok(())
1336}
1337
1338pub async fn resolve_with_services(
1343 params: TransitiveResolutionParams<'_>,
1344) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
1345 let TransitiveResolutionParams {
1346 ctx,
1347 core,
1348 base_deps,
1349 enable_transitive,
1350 prepared_versions,
1351 pattern_alias_map,
1352 services,
1353 progress,
1354 } = params;
1355 ctx.dependency_map.clear();
1357
1358 if !enable_transitive {
1359 return Ok(base_deps.to_vec());
1360 }
1361
1362 let graph = Arc::new(tokio::sync::Mutex::new(DependencyGraph::new()));
1363 let all_deps: Arc<DashMap<DependencyKey, ResourceDependency>> = Arc::new(DashMap::new());
1364 let processed: Arc<DashMap<DependencyKey, ()>> = Arc::new(DashMap::new()); type QueueItem = (String, ResourceDependency, Option<ResourceType>, String);
1368 #[allow(clippy::type_complexity)]
1369 let queue: Arc<tokio::sync::Mutex<Vec<QueueItem>>> =
1370 Arc::new(tokio::sync::Mutex::new(Vec::new()));
1371 let queue_len = Arc::new(AtomicUsize::new(0));
1373
1374 {
1376 let mut queue_guard = acquire_mutex_with_timeout(&queue, "transitive_queue").await?;
1377 for (name, dep, resource_type) in base_deps {
1378 let source = dep.get_source().map(std::string::ToString::to_string);
1379 let tool = Some(
1381 dep.get_tool()
1382 .map(std::string::ToString::to_string)
1383 .unwrap_or_else(|| ctx.base.manifest.get_default_tool(*resource_type)),
1384 );
1385
1386 let merged_variant_inputs =
1389 super::lockfile_builder::build_merged_variant_inputs(ctx.base.manifest, dep);
1390 let variant_hash = crate::utils::compute_variant_inputs_hash(&merged_variant_inputs)
1391 .unwrap_or_else(|_| crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string());
1392
1393 tracing::debug!(
1394 "[DEBUG] Adding base dep to queue: '{}' (type: {:?}, source: {:?}, tool: {:?}, is_local: {})",
1395 name,
1396 resource_type,
1397 source,
1398 tool,
1399 dep.is_local()
1400 );
1401 queue_guard.push((
1403 name.clone(),
1404 dep.clone(),
1405 Some(*resource_type),
1406 variant_hash.clone(),
1407 ));
1408 all_deps
1409 .insert((*resource_type, name.clone(), source, tool, variant_hash), dep.clone());
1410 }
1411 queue_len.store(queue_guard.len(), Ordering::SeqCst);
1413 }
1414
1415 let completed_counter = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
1417
1418 let cores = std::thread::available_parallelism().map(std::num::NonZero::get).unwrap_or(4);
1420 let max_concurrent = std::cmp::max(10, cores * 2);
1421
1422 let ctx_dependency_map = ctx.dependency_map;
1424 let ctx_custom_names = ctx.transitive_custom_names;
1425 let ctx_base = &ctx.base;
1426 let ctx_manifest_overrides = ctx.manifest_overrides;
1427
1428 loop {
1430 let batch: Vec<QueueEntry> = {
1432 let mut q = acquire_mutex_with_timeout(&queue, "transitive_queue").await?;
1433 let current_queue_len = q.len();
1434 let batch_size = std::cmp::min(max_concurrent, current_queue_len);
1435 if batch_size == 0 {
1436 break; }
1438 let mut batch_vec =
1440 q.drain(current_queue_len.saturating_sub(batch_size)..).collect::<Vec<_>>();
1441 batch_vec.reverse(); queue_len.fetch_sub(batch_vec.len(), Ordering::SeqCst);
1444 batch_vec
1445 };
1446
1447 let batch_futures: Vec<_> = batch
1449 .into_iter()
1450 .map(|(name, dep, resource_type, variant_hash)| {
1451 let graph_clone = Arc::clone(&graph);
1453 let all_deps_clone = Arc::clone(&all_deps);
1454 let processed_clone = Arc::clone(&processed);
1455 let queue_clone = Arc::clone(&queue);
1456 let queue_len_clone = Arc::clone(&queue_len);
1457 let pattern_alias_map_clone = Arc::clone(pattern_alias_map);
1458 let progress_clone = progress.clone();
1459 let counter_clone = Arc::clone(&completed_counter);
1460 let prepared_versions_clone = Arc::clone(prepared_versions);
1461 let dependency_map_clone = ctx_dependency_map;
1462 let custom_names_clone = ctx_custom_names;
1463 let manifest_overrides_clone = ctx_manifest_overrides;
1464
1465 async move {
1466 let resource_type = resource_type
1467 .expect("resource_type should always be threaded through queue");
1468
1469 let ctx = TransitiveProcessingContext {
1471 input: TransitiveInput {
1472 name,
1473 dep,
1474 resource_type,
1475 variant_hash,
1476 },
1477 shared: TransitiveSharedState {
1478 graph: graph_clone,
1479 all_deps: all_deps_clone,
1480 processed: processed_clone,
1481 queue: queue_clone,
1482 queue_len: queue_len_clone,
1483 pattern_alias_map: pattern_alias_map_clone,
1484 completed_counter: counter_clone,
1485 dependency_map: dependency_map_clone,
1486 custom_names: custom_names_clone,
1487 prepared_versions: &prepared_versions_clone,
1488 },
1489 resolution: TransitiveResolutionContext {
1490 ctx_base,
1491 manifest_overrides: manifest_overrides_clone,
1492 core,
1493 services,
1494 },
1495 progress: progress_clone,
1496 };
1497
1498 process_single_transitive_dependency(ctx).await
1499 }
1500 })
1501 .collect();
1502
1503 let timeout_duration = batch_operation_timeout();
1505 let results = tokio::time::timeout(timeout_duration, join_all(batch_futures))
1506 .await
1507 .with_context(|| {
1508 format!(
1509 "Batch transitive resolution timed out after {:?} - possible deadlock",
1510 timeout_duration
1511 )
1512 })?;
1513
1514 for result in results {
1516 result?;
1517 }
1518 }
1519
1520 acquire_mutex_with_timeout(&graph, "dependency_graph").await?.detect_cycles()?;
1522
1523 let ordered_nodes =
1525 acquire_mutex_with_timeout(&graph, "dependency_graph").await?.topological_order()?;
1526
1527 build_ordered_result(all_deps, ordered_nodes)
1529}
1530
1531fn create_skill_md_dependency(dep: &ResourceDependency) -> ResourceDependency {
1537 match dep {
1538 ResourceDependency::Simple(path) => {
1539 let skill_md_path = format!("{}/SKILL.md", path.trim_end_matches('/'));
1541 ResourceDependency::Simple(skill_md_path)
1542 }
1543 ResourceDependency::Detailed(detailed) => {
1544 let skill_md_path = format!("{}/SKILL.md", detailed.path.trim_end_matches('/'));
1546 ResourceDependency::Detailed(Box::new(DetailedDependency {
1547 path: skill_md_path,
1548 source: detailed.source.clone(),
1549 version: detailed.version.clone(),
1550 branch: detailed.branch.clone(),
1551 rev: detailed.rev.clone(),
1552 command: detailed.command.clone(),
1553 args: detailed.args.clone(),
1554 target: detailed.target.clone(),
1555 filename: detailed.filename.clone(),
1556 dependencies: detailed.dependencies.clone(),
1557 tool: detailed.tool.clone(),
1558 flatten: detailed.flatten,
1559 install: detailed.install,
1560 template_vars: detailed.template_vars.clone(),
1561 }))
1562 }
1563 }
1564}