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
199type CanonicalPathKey = (ResourceType, String, Option<String>, Option<String>, String);
202
203struct TransitiveSharedState<'a> {
212 graph: Arc<tokio::sync::Mutex<DependencyGraph>>,
213 all_deps: Arc<DashMap<DependencyKey, ResourceDependency>>,
214 processed: Arc<DashMap<DependencyKey, ()>>,
215 queue: Arc<tokio::sync::Mutex<Vec<QueueEntry>>>,
216 queue_len: Arc<AtomicUsize>,
219 pattern_alias_map: Arc<DashMap<(ResourceType, String), String>>,
220 completed_counter: Arc<AtomicUsize>,
221 dependency_map: &'a Arc<DashMap<DependencyKey, Vec<String>>>,
222 custom_names: &'a Arc<DashMap<DependencyKey, String>>,
223 prepared_versions: &'a Arc<DashMap<String, PreparedSourceVersion>>,
224 canonical_path_index: Arc<DashMap<CanonicalPathKey, String>>,
228}
229
230struct TransitiveResolutionContext<'a> {
236 ctx_base: &'a super::types::ResolutionContext<'a>,
237 manifest_overrides: &'a super::types::ManifestOverrideIndex,
238 core: &'a super::ResolutionCore,
239 services: &'a ResolutionServices<'a>,
240}
241
242async fn process_transitive_dependency_spec(
244 params: TransitiveDepProcessingParams<'_>,
245) -> Result<(ResourceDependency, String)> {
246 let parent_file_path = ResourceFetchingService::get_canonical_path(
248 params.core,
249 params.parent_dep,
250 params.version_service,
251 )
252 .await
253 .with_context(|| {
254 format!("Failed to get parent path for transitive dependencies of '{}'", params.parent_name)
255 })?;
256
257 let trans_canonical =
259 resolve_transitive_path(&parent_file_path, ¶ms.dep_spec.path, params.parent_name)?;
260
261 let trans_dep = create_transitive_dependency(
263 params.ctx,
264 params.parent_dep,
265 params.dep_resource_type,
266 params.parent_resource_type,
267 params.parent_name,
268 params.dep_spec,
269 &parent_file_path,
270 &trans_canonical,
271 params.prepared_versions,
272 )
273 .await?;
274
275 let trans_name = if trans_dep.get_source().is_none() {
277 let manifest_dir = params
281 .ctx
282 .base
283 .manifest
284 .manifest_dir
285 .as_ref()
286 .ok_or_else(|| anyhow::anyhow!("Manifest directory not available"))?;
287
288 let source_context = crate::resolver::source_context::SourceContext::local(manifest_dir);
289 generate_dependency_name(trans_dep.get_path(), &source_context)
290 } else {
291 let source_name = trans_dep
293 .get_source()
294 .ok_or_else(|| anyhow::anyhow!("Git dependency missing source name"))?;
295 let source_context = crate::resolver::source_context::SourceContext::remote(source_name);
296 generate_dependency_name(trans_dep.get_path(), &source_context)
297 };
298
299 Ok((trans_dep, trans_name))
300}
301
302fn resolve_transitive_path(
304 parent_file_path: &Path,
305 dep_path: &str,
306 parent_name: &str,
307) -> Result<PathBuf> {
308 let is_pattern = dep_path.contains('*') || dep_path.contains('?') || dep_path.contains('[');
310
311 if is_pattern {
312 let parent_dir = parent_file_path.parent().ok_or_else(|| {
314 anyhow::anyhow!(
315 "Failed to resolve transitive dependency '{}' for '{}': parent file has no directory",
316 dep_path,
317 parent_name
318 )
319 })?;
320 let resolved = parent_dir.join(dep_path);
321
322 let mut result = PathBuf::new();
324 for component in resolved.components() {
325 match component {
326 std::path::Component::RootDir => result.push(component),
327 std::path::Component::ParentDir => {
328 result.pop();
329 }
330 std::path::Component::CurDir => {}
331 _ => result.push(component),
332 }
333 }
334 Ok(result)
335 } else if is_file_relative_path(dep_path) || !dep_path.contains('/') {
336 let parent_dir = parent_file_path.parent().ok_or_else(|| {
339 anyhow::anyhow!(
340 "Failed to resolve transitive dependency '{}' for '{}': parent file has no directory",
341 dep_path,
342 parent_name
343 )
344 })?;
345
346 let resolved = parent_dir.join(dep_path);
347 resolved.canonicalize().map_err(|e| {
348 let file_error = crate::core::file_error::FileOperationError::new(
350 crate::core::file_error::FileOperationContext::new(
351 crate::core::file_error::FileOperation::Canonicalize,
352 &resolved,
353 format!("resolving transitive dependency '{}' for '{}'", dep_path, parent_name),
354 "transitive_resolver::resolve_transitive_path",
355 ),
356 e,
357 );
358 anyhow::Error::from(file_error)
359 })
360 } else {
361 resolve_repo_relative_path(parent_file_path, dep_path, parent_name)
363 }
364}
365
366fn resolve_repo_relative_path(
368 parent_file_path: &Path,
369 dep_path: &str,
370 parent_name: &str,
371) -> Result<PathBuf> {
372 let repo_root = parent_file_path
374 .ancestors()
375 .find(|p| {
376 let git_path = p.join(".git");
379 git_path.is_file()
380 })
381 .or_else(|| parent_file_path.ancestors().nth(2)) .ok_or_else(|| {
383 anyhow::anyhow!(
384 "Failed to find repository root for transitive dependency '{}'",
385 dep_path
386 )
387 })?;
388
389 let full_path = repo_root.join(dep_path);
390 full_path.canonicalize().with_context(|| {
391 format!(
392 "Failed to resolve repo-relative transitive dependency '{}' for '{}': {} (repo root: {})",
393 dep_path,
394 parent_name,
395 full_path.display(),
396 repo_root.display()
397 )
398 })
399}
400
401#[allow(clippy::too_many_arguments)]
403async fn create_transitive_dependency(
404 ctx: &TransitiveContext<'_>,
405 parent_dep: &ResourceDependency,
406 dep_resource_type: ResourceType,
407 parent_resource_type: ResourceType,
408 _parent_name: &str,
409 dep_spec: &crate::manifest::DependencySpec,
410 parent_file_path: &Path,
411 trans_canonical: &Path,
412 prepared_versions: &Arc<DashMap<String, PreparedSourceVersion>>,
413) -> Result<ResourceDependency> {
414 use super::types::{OverrideKey, normalize_lookup_path};
415
416 let mut dep = if parent_dep.get_source().is_none() {
418 create_path_only_transitive_dep(
419 ctx,
420 parent_dep,
421 dep_resource_type,
422 parent_resource_type,
423 dep_spec,
424 trans_canonical,
425 )?
426 } else {
427 create_git_backed_transitive_dep(
428 ctx,
429 parent_dep,
430 dep_resource_type,
431 parent_resource_type,
432 dep_spec,
433 parent_file_path,
434 trans_canonical,
435 prepared_versions,
436 )
437 .await?
438 };
439
440 let normalized_path = normalize_lookup_path(dep.get_path());
442 let source = dep.get_source().map(std::string::ToString::to_string);
443
444 let tool = dep
446 .get_tool()
447 .map(str::to_string)
448 .unwrap_or_else(|| ctx.base.manifest.get_default_tool(dep_resource_type));
449
450 let variant_hash =
451 super::lockfile_builder::compute_merged_variant_hash(ctx.base.manifest, &dep);
452
453 let override_key = OverrideKey {
454 resource_type: dep_resource_type,
455 normalized_path: normalized_path.clone(),
456 source,
457 tool,
458 variant_hash,
459 };
460
461 if let Some(override_info) = ctx.manifest_overrides.get(&override_key) {
463 apply_manifest_override(&mut dep, override_info, &normalized_path);
464 }
465
466 Ok(dep)
467}
468
469fn create_path_only_transitive_dep(
471 ctx: &TransitiveContext<'_>,
472 parent_dep: &ResourceDependency,
473 dep_resource_type: ResourceType,
474 parent_resource_type: ResourceType,
475 dep_spec: &crate::manifest::DependencySpec,
476 trans_canonical: &Path,
477) -> Result<ResourceDependency> {
478 let manifest_dir = ctx.base.manifest.manifest_dir.as_ref().ok_or_else(|| {
479 anyhow::anyhow!("Manifest directory not available for path-only transitive dep")
480 })?;
481
482 let dep_path_str = match manifest_dir.canonicalize() {
484 Ok(canonical_manifest) => {
485 utils::compute_relative_path(&canonical_manifest, trans_canonical)
486 }
487 Err(e) => {
488 eprintln!(
489 "Warning: Could not canonicalize manifest directory {}: {}. Using non-canonical path.",
490 manifest_dir.display(),
491 e
492 );
493 utils::compute_relative_path(manifest_dir, trans_canonical)
494 }
495 };
496
497 let trans_tool = determine_transitive_tool(
499 ctx,
500 parent_dep,
501 dep_spec,
502 parent_resource_type,
503 dep_resource_type,
504 );
505
506 Ok(ResourceDependency::Detailed(Box::new(DetailedDependency {
507 source: None,
508 path: utils::normalize_path_for_storage(dep_path_str),
509 version: None,
510 branch: None,
511 rev: None,
512 command: None,
513 args: None,
514 target: None,
515 filename: None,
516 dependencies: None,
517 tool: trans_tool,
518 flatten: None,
519 install: dep_spec.install.or(Some(true)),
520 template_vars: Some(super::lockfile_builder::build_merged_variant_inputs(
521 ctx.base.manifest,
522 parent_dep,
523 )),
524 })))
525}
526
527#[allow(clippy::too_many_arguments)]
529async fn create_git_backed_transitive_dep(
530 ctx: &TransitiveContext<'_>,
531 parent_dep: &ResourceDependency,
532 dep_resource_type: ResourceType,
533 parent_resource_type: ResourceType,
534 dep_spec: &crate::manifest::DependencySpec,
535 parent_file_path: &Path,
536 trans_canonical: &Path,
537 _prepared_versions: &Arc<DashMap<String, PreparedSourceVersion>>,
538) -> Result<ResourceDependency> {
539 let source_name = parent_dep
540 .get_source()
541 .ok_or_else(|| anyhow::anyhow!("Expected source for Git-backed dependency"))?;
542 let source_url = ctx
543 .base
544 .source_manager
545 .get_source_url(source_name)
546 .ok_or_else(|| anyhow::anyhow!("Source '{source_name}' not found"))?;
547
548 let repo_relative = if utils::is_local_path(&source_url) {
550 strip_local_source_prefix(&source_url, trans_canonical)?
551 } else {
552 strip_git_worktree_prefix_from_parent(parent_file_path, trans_canonical)?
554 };
555
556 let trans_tool = determine_transitive_tool(
558 ctx,
559 parent_dep,
560 dep_spec,
561 parent_resource_type,
562 dep_resource_type,
563 );
564
565 Ok(ResourceDependency::Detailed(Box::new(DetailedDependency {
566 source: Some(source_name.to_string()),
567 path: utils::normalize_path_for_storage(repo_relative.to_string_lossy().to_string()),
568 version: dep_spec
569 .version
570 .clone()
571 .or_else(|| parent_dep.get_version().map(|v| v.to_string())),
572 branch: None,
573 rev: None,
574 command: None,
575 args: None,
576 target: None,
577 filename: None,
578 dependencies: None,
579 tool: trans_tool,
580 flatten: None,
581 install: dep_spec.install.or(Some(true)),
582 template_vars: Some(super::lockfile_builder::build_merged_variant_inputs(
583 ctx.base.manifest,
584 parent_dep,
585 )),
586 })))
587}
588
589fn strip_local_source_prefix(source_url: &str, trans_canonical: &Path) -> Result<PathBuf> {
591 let source_url_path = PathBuf::from(source_url);
592 let source_path = source_url_path.canonicalize().map_err(|e| {
593 let file_error = crate::core::file_error::FileOperationError::new(
594 crate::core::file_error::FileOperationContext::new(
595 crate::core::file_error::FileOperation::Canonicalize,
596 &source_url_path,
597 "canonicalizing local source path for transitive dependency".to_string(),
598 "transitive_resolver::strip_local_source_prefix",
599 ),
600 e,
601 );
602 anyhow::Error::from(file_error)
603 })?;
604
605 let trans_str = trans_canonical.to_string_lossy();
607 let is_pattern = trans_str.contains('*') || trans_str.contains('?') || trans_str.contains('[');
608
609 if is_pattern {
610 let parent_dir = trans_canonical.parent().ok_or_else(|| {
612 anyhow::anyhow!("Pattern path has no parent directory: {}", trans_canonical.display())
613 })?;
614 let filename = trans_canonical.file_name().ok_or_else(|| {
615 anyhow::anyhow!("Pattern path has no filename: {}", trans_canonical.display())
616 })?;
617
618 let canonical_dir = parent_dir.canonicalize().map_err(|e| {
620 let file_error = crate::core::file_error::FileOperationError::new(
621 crate::core::file_error::FileOperationContext::new(
622 crate::core::file_error::FileOperation::Canonicalize,
623 parent_dir,
624 "canonicalizing pattern directory for local source".to_string(),
625 "transitive_resolver::strip_local_source_prefix",
626 ),
627 e,
628 );
629 anyhow::Error::from(file_error)
630 })?;
631
632 let canonical_pattern = canonical_dir.join(filename);
634
635 canonical_pattern
637 .strip_prefix(&source_path)
638 .with_context(|| {
639 format!(
640 "Transitive pattern dep outside parent's source: {} not under {}",
641 canonical_pattern.display(),
642 source_path.display()
643 )
644 })
645 .map(|p| p.to_path_buf())
646 } else {
647 trans_canonical
648 .strip_prefix(&source_path)
649 .with_context(|| {
650 format!(
651 "Transitive dep resolved outside parent's source directory: {} not under {}",
652 trans_canonical.display(),
653 source_path.display()
654 )
655 })
656 .map(|p| p.to_path_buf())
657 }
658}
659
660fn strip_git_worktree_prefix_from_parent(
663 parent_file_path: &Path,
664 trans_canonical: &Path,
665) -> Result<PathBuf> {
666 let worktree_root = parent_file_path
670 .ancestors()
671 .find(|p| {
672 let git_path = p.join(".git");
673 git_path.is_file()
674 })
675 .ok_or_else(|| {
676 anyhow::anyhow!(
677 "Failed to find worktree root from parent file: {}",
678 parent_file_path.display()
679 )
680 })?;
681
682 let canonical_worktree = worktree_root.canonicalize().map_err(|e| {
684 let file_error = crate::core::file_error::FileOperationError::new(
685 crate::core::file_error::FileOperationContext::new(
686 crate::core::file_error::FileOperation::Canonicalize,
687 worktree_root,
688 "canonicalizing worktree root for transitive dependency".to_string(),
689 "transitive_resolver::strip_git_worktree_prefix_from_parent",
690 ),
691 e,
692 );
693 anyhow::Error::from(file_error)
694 })?;
695
696 let trans_str = trans_canonical.to_string_lossy();
698 let is_pattern = trans_str.contains('*') || trans_str.contains('?') || trans_str.contains('[');
699
700 if is_pattern {
701 let parent_dir = trans_canonical.parent().ok_or_else(|| {
703 anyhow::anyhow!("Pattern path has no parent directory: {}", trans_canonical.display())
704 })?;
705 let filename = trans_canonical.file_name().ok_or_else(|| {
706 anyhow::anyhow!("Pattern path has no filename: {}", trans_canonical.display())
707 })?;
708
709 let canonical_dir = parent_dir.canonicalize().map_err(|e| {
711 let file_error = crate::core::file_error::FileOperationError::new(
712 crate::core::file_error::FileOperationContext::new(
713 crate::core::file_error::FileOperation::Canonicalize,
714 parent_dir,
715 "canonicalizing pattern directory for Git worktree".to_string(),
716 "transitive_resolver::strip_git_worktree_prefix_from_parent",
717 ),
718 e,
719 );
720 anyhow::Error::from(file_error)
721 })?;
722
723 let canonical_pattern = canonical_dir.join(filename);
725
726 canonical_pattern
728 .strip_prefix(&canonical_worktree)
729 .with_context(|| {
730 format!(
731 "Transitive pattern dep outside parent's worktree: {} not under {}",
732 canonical_pattern.display(),
733 canonical_worktree.display()
734 )
735 })
736 .map(|p| p.to_path_buf())
737 } else {
738 trans_canonical
739 .strip_prefix(&canonical_worktree)
740 .with_context(|| {
741 format!(
742 "Transitive dep outside parent's worktree: {} not under {}",
743 trans_canonical.display(),
744 canonical_worktree.display()
745 )
746 })
747 .map(|p| p.to_path_buf())
748 }
749}
750
751fn determine_transitive_tool(
753 ctx: &TransitiveContext<'_>,
754 parent_dep: &ResourceDependency,
755 dep_spec: &crate::manifest::DependencySpec,
756 parent_resource_type: ResourceType,
757 dep_resource_type: ResourceType,
758) -> Option<String> {
759 if let Some(explicit_tool) = &dep_spec.tool {
760 Some(explicit_tool.clone())
761 } else {
762 let parent_tool = parent_dep
763 .get_tool()
764 .map(str::to_string)
765 .unwrap_or_else(|| ctx.base.manifest.get_default_tool(parent_resource_type));
766 if ctx.base.manifest.is_resource_supported(&parent_tool, dep_resource_type) {
767 Some(parent_tool)
768 } else {
769 Some(ctx.base.manifest.get_default_tool(dep_resource_type))
770 }
771 }
772}
773
774fn build_ordered_result(
776 all_deps: Arc<DashMap<DependencyKey, ResourceDependency>>,
777 ordered_nodes: Vec<DependencyNode>,
778) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
779 let mut result = Vec::new();
780 let mut added_keys = HashSet::new();
781
782 tracing::debug!(
783 "Transitive resolution - topological order has {} nodes, all_deps has {} entries",
784 ordered_nodes.len(),
785 all_deps.len()
786 );
787
788 for node in ordered_nodes {
789 tracing::debug!(
790 "Processing ordered node: {}/{} (source: {:?})",
791 node.resource_type,
792 node.name,
793 node.source
794 );
795
796 for entry in all_deps.iter() {
798 let (key, dep) = (entry.key(), entry.value());
799 if key.0 == node.resource_type && key.1 == node.name && key.2 == node.source {
800 tracing::debug!(
801 " -> Found match in all_deps, adding to result with type {:?}",
802 node.resource_type
803 );
804 result.push((node.name.clone(), dep.clone(), node.resource_type));
805 added_keys.insert(key.clone());
806 break;
807 }
808 }
809 }
810
811 for entry in all_deps.iter() {
813 let (key, dep) = (entry.key(), entry.value());
814 if !added_keys.contains(key) && !dep.is_pattern() {
815 tracing::debug!(
816 "Adding non-graph dependency: {}/{} (source: {:?}) with type {:?}",
817 key.0,
818 key.1,
819 key.2,
820 key.0
821 );
822 result.push((key.1.clone(), dep.clone(), key.0));
823 }
824 }
825
826 tracing::debug!("Transitive resolution returning {} dependencies", result.len());
827
828 Ok(result)
829}
830
831pub fn group_key(source: &str, version: &str) -> String {
833 format!("{source}::{version}")
834}
835
836async fn process_single_transitive_dependency<'a>(
841 ctx: TransitiveProcessingContext<'a>,
842) -> Result<()> {
843 let source = ctx.input.dep.get_source().map(std::string::ToString::to_string);
844 let tool =
847 Some(ctx.input.dep.get_tool().map(std::string::ToString::to_string).unwrap_or_else(|| {
848 ctx.resolution.ctx_base.manifest.get_default_tool(ctx.input.resource_type)
849 }));
850
851 let canonical_name = if source.is_none() {
855 let manifest_dir = ctx
857 .resolution
858 .ctx_base
859 .manifest
860 .manifest_dir
861 .as_deref()
862 .unwrap_or(std::path::Path::new("."));
863 let source_context = crate::resolver::source_context::SourceContext::local(manifest_dir);
864 generate_dependency_name(ctx.input.dep.get_path(), &source_context)
865 } else {
866 let source_name = source.as_deref().unwrap_or("unknown");
868 let source_context = crate::resolver::source_context::SourceContext::remote(source_name);
869 generate_dependency_name(ctx.input.dep.get_path(), &source_context)
870 };
871
872 let key = (
873 ctx.input.resource_type,
874 ctx.input.name.clone(),
875 source.clone(),
876 tool.clone(),
877 ctx.input.variant_hash.clone(),
878 );
879
880 let display_name = if source.is_some() {
882 if let Some(version) = ctx.input.dep.get_version() {
883 format!("{}@{}", ctx.input.name, version)
884 } else {
885 format!("{}@HEAD", ctx.input.name)
886 }
887 } else {
888 ctx.input.name.clone()
889 };
890 let progress_key = format!("{}:{}", ctx.input.resource_type, &display_name);
891
892 if let Some(ref pm) = ctx.progress {
894 pm.mark_item_active(&display_name, &progress_key);
895 }
896
897 tracing::debug!(
898 "[TRANSITIVE] Processing: '{}' (type: {:?}, source: {:?})",
899 ctx.input.name,
900 ctx.input.resource_type,
901 source
902 );
903
904 let is_stale = ctx
909 .shared
910 .all_deps
911 .get(&key)
912 .map(|current_dep| current_dep.get_version() != ctx.input.dep.get_version())
913 .unwrap_or(false);
914
915 if is_stale {
916 tracing::debug!("[TRANSITIVE] Skipped stale: '{}'", ctx.input.name);
917 if let Some(ref pm) = ctx.progress {
919 let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
920 let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
921 pm.mark_item_complete(
922 &progress_key,
923 Some(&display_name),
924 completed,
925 total,
926 "Scanning dependencies",
927 );
928 }
929 return Ok(());
930 }
931
932 if ctx.shared.processed.contains_key(&key) {
933 tracing::debug!("[TRANSITIVE] Already processed: '{}'", ctx.input.name);
934 if let Some(ref pm) = ctx.progress {
935 let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
936 let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
937 pm.mark_item_complete(
938 &progress_key,
939 Some(&display_name),
940 completed,
941 total,
942 "Scanning dependencies",
943 );
944 }
945 return Ok(());
946 }
947
948 ctx.shared.processed.insert(key.clone(), ());
949
950 if ctx.input.dep.is_pattern() {
952 tracing::debug!("[TRANSITIVE] Expanding pattern: '{}'", ctx.input.name);
953 match ctx
954 .resolution
955 .services
956 .pattern_service
957 .expand_pattern(
958 ctx.resolution.core,
959 &ctx.input.dep,
960 ctx.input.resource_type,
961 ctx.shared.prepared_versions.as_ref(),
962 )
963 .await
964 {
965 Ok(concrete_deps) => {
966 let mut items_to_queue = Vec::new();
970
971 for (concrete_name, concrete_dep) in concrete_deps {
972 ctx.shared.pattern_alias_map.insert(
973 (ctx.input.resource_type, concrete_name.clone()),
974 ctx.input.name.clone(),
975 );
976
977 let concrete_source =
978 concrete_dep.get_source().map(std::string::ToString::to_string);
979 let concrete_tool =
980 concrete_dep.get_tool().map(std::string::ToString::to_string);
981 let concrete_variant_hash =
982 super::lockfile_builder::compute_merged_variant_hash(
983 ctx.resolution.ctx_base.manifest,
984 &concrete_dep,
985 );
986 let concrete_key = (
987 ctx.input.resource_type,
988 concrete_name.clone(),
989 concrete_source,
990 concrete_tool,
991 concrete_variant_hash.clone(),
992 );
993
994 match ctx.shared.all_deps.entry(concrete_key) {
996 dashmap::mapref::entry::Entry::Vacant(e) => {
997 e.insert(concrete_dep.clone());
998 items_to_queue.push((
1000 concrete_name,
1001 concrete_dep,
1002 Some(ctx.input.resource_type),
1003 concrete_variant_hash,
1004 ));
1005 }
1006 dashmap::mapref::entry::Entry::Occupied(mut e) => {
1007 let existing = e.get();
1009 if should_replace_existing(existing, &concrete_dep) {
1010 tracing::debug!(
1011 "[PATTERN] Replacing existing dep '{}' with semver version",
1012 concrete_name
1013 );
1014 e.insert(concrete_dep.clone());
1015 items_to_queue.push((
1016 concrete_name,
1017 concrete_dep,
1018 Some(ctx.input.resource_type),
1019 concrete_variant_hash,
1020 ));
1021 }
1022 }
1023 }
1024 }
1026
1027 if !items_to_queue.is_empty() {
1029 let items_count = items_to_queue.len();
1030 let mut queue =
1031 acquire_mutex_with_timeout(&ctx.shared.queue, "transitive_queue").await?;
1032 queue.extend(items_to_queue);
1033 ctx.shared.queue_len.fetch_add(items_count, Ordering::SeqCst);
1035 }
1036 }
1037 Err(e) => {
1038 anyhow::bail!("Failed to expand pattern '{}': {}", ctx.input.dep.get_path(), e);
1039 }
1040 }
1041 if let Some(ref pm) = ctx.progress {
1043 let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
1044 let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
1045 pm.mark_item_complete(
1046 &progress_key,
1047 Some(&display_name),
1048 completed,
1049 total,
1050 "Scanning dependencies",
1051 );
1052 }
1053 return Ok(());
1054 }
1055
1056 let content = if ctx.input.resource_type == ResourceType::Skill {
1059 let skill_md_dep = create_skill_md_dependency(&ctx.input.dep);
1061 ResourceFetchingService::fetch_content(
1062 ctx.resolution.core,
1063 &skill_md_dep,
1064 ctx.resolution.services.version_service,
1065 )
1066 .await
1067 .with_context(|| {
1068 format!(
1069 "Failed to fetch SKILL.md for skill '{}' ({})",
1070 ctx.input.name,
1071 ctx.input.dep.get_path()
1072 )
1073 })?
1074 } else {
1075 ResourceFetchingService::fetch_content(
1076 ctx.resolution.core,
1077 &ctx.input.dep,
1078 ctx.resolution.services.version_service,
1079 )
1080 .await
1081 .with_context(|| {
1082 format!(
1083 "Failed to fetch resource '{}' ({}) for transitive deps",
1084 ctx.input.name,
1085 ctx.input.dep.get_path()
1086 )
1087 })?
1088 };
1089
1090 tracing::debug!(
1095 "[TRANSITIVE] Fetched content for '{}' ({} bytes)",
1096 ctx.input.name,
1097 content.len()
1098 );
1099
1100 let variant_inputs_value = super::lockfile_builder::build_merged_variant_inputs(
1103 ctx.resolution.ctx_base.manifest,
1104 &ctx.input.dep,
1105 );
1106 let variant_inputs = Some(&variant_inputs_value);
1107
1108 let path = if ctx.input.resource_type == ResourceType::Skill {
1111 PathBuf::from(format!("{}/SKILL.md", ctx.input.dep.get_path().trim_end_matches('/')))
1112 } else {
1113 PathBuf::from(ctx.input.dep.get_path())
1114 };
1115 let metadata = MetadataExtractor::extract(
1116 &path,
1117 &content,
1118 variant_inputs,
1119 ctx.resolution.ctx_base.operation_context.map(|arc| arc.as_ref()),
1120 )?;
1121
1122 tracing::debug!(
1123 "[DEBUG] Extracted metadata for '{}': has_deps={}",
1124 ctx.input.name,
1125 metadata.get_dependencies().is_some()
1126 );
1127
1128 if let Some(deps_map) = metadata.get_dependencies() {
1130 tracing::debug!(
1131 "[DEBUG] Found {} dependency type(s) for '{}': {:?}",
1132 deps_map.len(),
1133 ctx.input.name,
1134 deps_map.keys().collect::<Vec<_>>()
1135 );
1136
1137 let mut items_to_queue = Vec::new();
1141
1142 let mut graph_edges: Vec<(DependencyNode, DependencyNode)> = Vec::new();
1146
1147 let declared_count = metadata.dependency_count();
1149 let declared_deps: Vec<(String, String)> = deps_map
1150 .iter()
1151 .flat_map(|(rtype, specs)| specs.iter().map(move |s| (rtype.clone(), s.path.clone())))
1152 .collect();
1153
1154 for (dep_resource_type_str, dep_specs) in deps_map {
1155 let dep_resource_type: ResourceType =
1156 dep_resource_type_str.parse().unwrap_or(ResourceType::Snippet);
1157
1158 for dep_spec in dep_specs {
1159 let mut dummy_conflict_detector = ConflictDetector::new();
1162 let temp_ctx = super::types::TransitiveContext {
1163 base: *ctx.resolution.ctx_base,
1164 dependency_map: ctx.shared.dependency_map,
1165 transitive_custom_names: ctx.shared.custom_names,
1166 conflict_detector: &mut dummy_conflict_detector,
1167 manifest_overrides: ctx.resolution.manifest_overrides,
1168 };
1169
1170 let (trans_dep, trans_name) =
1172 process_transitive_dependency_spec(TransitiveDepProcessingParams {
1173 ctx: &temp_ctx,
1174 core: ctx.resolution.core,
1175 parent_dep: &ctx.input.dep,
1176 dep_resource_type,
1177 parent_resource_type: ctx.input.resource_type,
1178 parent_name: &ctx.input.name,
1179 dep_spec,
1180 version_service: ctx.resolution.services.version_service,
1181 prepared_versions: ctx.shared.prepared_versions,
1182 })
1183 .await?;
1184
1185 let trans_source = trans_dep.get_source().map(std::string::ToString::to_string);
1186 let trans_tool = Some(
1189 trans_dep.get_tool().map(std::string::ToString::to_string).unwrap_or_else(
1190 || ctx.resolution.ctx_base.manifest.get_default_tool(dep_resource_type),
1191 ),
1192 );
1193 let trans_variant_hash = super::lockfile_builder::compute_merged_variant_hash(
1194 ctx.resolution.ctx_base.manifest,
1195 &trans_dep,
1196 );
1197
1198 let canonical_path = super::types::normalize_lookup_path(trans_dep.get_path());
1201 let canonical_lookup_key = (
1202 dep_resource_type,
1203 canonical_path.clone(),
1204 trans_source.clone(),
1205 trans_tool.clone(),
1206 trans_variant_hash.clone(),
1207 );
1208
1209 let effective_name = if let Some(manifest_alias) =
1212 ctx.shared.canonical_path_index.get(&canonical_lookup_key)
1213 {
1214 let alias = manifest_alias.value().clone();
1215 tracing::debug!(
1216 "[TRANSITIVE] Transitive dep '{}' matches manifest dep '{}' - using alias for deduplication",
1217 trans_name,
1218 alias
1219 );
1220 alias
1221 } else {
1222 trans_name.clone()
1223 };
1224
1225 let trans_key = (
1227 dep_resource_type,
1228 effective_name.clone(),
1229 trans_source.clone(),
1230 trans_tool.clone(),
1231 trans_variant_hash.clone(),
1232 );
1233
1234 let graph_dep_name = trans_name.clone();
1236
1237 tracing::debug!(
1238 "[TRANSITIVE] Found transitive dep '{}' (type: {:?}, tool: {:?}, parent: {})",
1239 trans_name,
1240 dep_resource_type,
1241 trans_tool,
1242 ctx.input.name
1243 );
1244
1245 if let Some(custom_name) = &dep_spec.name {
1247 ctx.shared.custom_names.insert(trans_key.clone(), custom_name.clone());
1248 tracing::debug!(
1249 "Storing custom name '{}' for transitive dep '{}'",
1250 custom_name,
1251 trans_name
1252 );
1253 }
1254
1255 let from_node = DependencyNode::with_source(
1260 ctx.input.resource_type,
1261 &canonical_name,
1262 source.clone(),
1263 );
1264 let to_node = DependencyNode::with_source(
1265 dep_resource_type,
1266 &graph_dep_name,
1267 trans_source.clone(),
1268 );
1269 graph_edges.push((from_node, to_node));
1270
1271 let from_key = (
1273 ctx.input.resource_type,
1274 ctx.input.name.clone(),
1275 source.clone(),
1276 tool.clone(),
1277 ctx.input.variant_hash.clone(),
1278 );
1279 let dep_ref =
1280 LockfileDependencyRef::local(dep_resource_type, graph_dep_name.clone(), None)
1281 .to_string();
1282 tracing::debug!(
1283 "[DEBUG] Adding to dependency_map: parent='{}' (type={:?}, source={:?}, tool={:?}, hash={}), child='{}' (type={:?})",
1284 ctx.input.name,
1285 ctx.input.resource_type,
1286 source,
1287 tool,
1288 &ctx.input.variant_hash[..8],
1289 dep_ref,
1290 dep_resource_type
1291 );
1292 ctx.shared.dependency_map.entry(from_key).or_default().push(dep_ref);
1293
1294 match ctx.shared.all_deps.entry(trans_key) {
1300 dashmap::mapref::entry::Entry::Vacant(e) => {
1301 tracing::debug!(
1303 "Adding transitive dep '{}' (parent: {})",
1304 trans_name,
1305 ctx.input.name
1306 );
1307 e.insert(trans_dep.clone());
1308 items_to_queue.push((
1310 trans_name,
1311 trans_dep,
1312 Some(dep_resource_type),
1313 trans_variant_hash,
1314 ));
1315 }
1316 dashmap::mapref::entry::Entry::Occupied(mut e) => {
1317 let existing = e.get();
1320 if should_replace_existing(existing, &trans_dep) {
1321 tracing::debug!(
1322 "[TRANSITIVE] Replacing existing dep '{}' (version: {:?}) with semver version {:?}",
1323 trans_name,
1324 existing.get_version(),
1325 trans_dep.get_version()
1326 );
1327 e.insert(trans_dep.clone());
1328 items_to_queue.push((
1330 trans_name,
1331 trans_dep,
1332 Some(dep_resource_type),
1333 trans_variant_hash,
1334 ));
1335 } else {
1336 tracing::debug!(
1337 "[TRANSITIVE] Keeping existing dep '{}' (version: {:?} vs new {:?})",
1338 trans_name,
1339 existing.get_version(),
1340 trans_dep.get_version()
1341 );
1342 }
1343 }
1344 }
1345 }
1347 }
1348
1349 let resolved_count = graph_edges.len();
1351
1352 if !graph_edges.is_empty() {
1354 let mut graph =
1355 acquire_mutex_with_timeout(&ctx.shared.graph, "dependency_graph").await?;
1356 for (from_node, to_node) in graph_edges {
1357 graph.add_dependency(from_node, to_node);
1358 }
1359 }
1360
1361 if !items_to_queue.is_empty() {
1363 let items_count = items_to_queue.len();
1364 let mut queue =
1365 acquire_mutex_with_timeout(&ctx.shared.queue, "transitive_queue").await?;
1366 queue.extend(items_to_queue);
1367 ctx.shared.queue_len.fetch_add(items_count, Ordering::SeqCst);
1369 }
1370
1371 if resolved_count < declared_count {
1373 return Err(crate::core::AgpmError::DependencyResolutionMismatch {
1374 resource: ctx.input.name.clone(),
1375 declared_count,
1376 resolved_count,
1377 declared_deps,
1378 }
1379 .into());
1380 }
1381 }
1382
1383 if let Some(ref pm) = ctx.progress {
1385 let completed = ctx.shared.completed_counter.fetch_add(1, Ordering::SeqCst) + 1;
1386 let total = completed + ctx.shared.queue_len.load(Ordering::SeqCst);
1387 pm.mark_item_complete(
1388 &progress_key,
1389 Some(&display_name),
1390 completed,
1391 total,
1392 "Scanning dependencies",
1393 );
1394 }
1395
1396 Ok(())
1397}
1398
1399pub async fn resolve_with_services(
1404 params: TransitiveResolutionParams<'_>,
1405) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
1406 let TransitiveResolutionParams {
1407 ctx,
1408 core,
1409 base_deps,
1410 enable_transitive,
1411 prepared_versions,
1412 pattern_alias_map,
1413 services,
1414 progress,
1415 } = params;
1416 ctx.dependency_map.clear();
1418
1419 if !enable_transitive {
1420 return Ok(base_deps.to_vec());
1421 }
1422
1423 let graph = Arc::new(tokio::sync::Mutex::new(DependencyGraph::new()));
1424 let all_deps: Arc<DashMap<DependencyKey, ResourceDependency>> = Arc::new(DashMap::new());
1425 let processed: Arc<DashMap<DependencyKey, ()>> = Arc::new(DashMap::new()); let canonical_path_index: Arc<DashMap<CanonicalPathKey, String>> = Arc::new(DashMap::new());
1428
1429 type QueueItem = (String, ResourceDependency, Option<ResourceType>, String);
1431 #[allow(clippy::type_complexity)]
1432 let queue: Arc<tokio::sync::Mutex<Vec<QueueItem>>> =
1433 Arc::new(tokio::sync::Mutex::new(Vec::new()));
1434 let queue_len = Arc::new(AtomicUsize::new(0));
1436
1437 {
1439 let mut queue_guard = acquire_mutex_with_timeout(&queue, "transitive_queue").await?;
1440 for (name, dep, resource_type) in base_deps {
1441 let source = dep.get_source().map(std::string::ToString::to_string);
1442 let tool = Some(
1444 dep.get_tool()
1445 .map(std::string::ToString::to_string)
1446 .unwrap_or_else(|| ctx.base.manifest.get_default_tool(*resource_type)),
1447 );
1448
1449 let merged_variant_inputs =
1452 super::lockfile_builder::build_merged_variant_inputs(ctx.base.manifest, dep);
1453 let variant_hash = crate::utils::compute_variant_inputs_hash(&merged_variant_inputs)
1454 .unwrap_or_else(|_| crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string());
1455
1456 tracing::debug!(
1457 "[DEBUG] Adding base dep to queue: '{}' (type: {:?}, source: {:?}, tool: {:?}, is_local: {})",
1458 name,
1459 resource_type,
1460 source,
1461 tool,
1462 dep.is_local()
1463 );
1464 queue_guard.push((
1466 name.clone(),
1467 dep.clone(),
1468 Some(*resource_type),
1469 variant_hash.clone(),
1470 ));
1471 all_deps.insert(
1472 (*resource_type, name.clone(), source.clone(), tool.clone(), variant_hash.clone()),
1473 dep.clone(),
1474 );
1475
1476 let canonical_path = super::types::normalize_lookup_path(dep.get_path());
1480 canonical_path_index
1481 .insert((*resource_type, canonical_path, source, tool, variant_hash), name.clone());
1482 }
1483 queue_len.store(queue_guard.len(), Ordering::SeqCst);
1485 }
1486
1487 let completed_counter = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
1489
1490 let cores = std::thread::available_parallelism().map(std::num::NonZero::get).unwrap_or(4);
1492 let max_concurrent = std::cmp::max(10, cores * 2);
1493
1494 let ctx_dependency_map = ctx.dependency_map;
1496 let ctx_custom_names = ctx.transitive_custom_names;
1497 let ctx_base = &ctx.base;
1498 let ctx_manifest_overrides = ctx.manifest_overrides;
1499
1500 loop {
1502 let batch: Vec<QueueEntry> = {
1504 let mut q = acquire_mutex_with_timeout(&queue, "transitive_queue").await?;
1505 let current_queue_len = q.len();
1506 let batch_size = std::cmp::min(max_concurrent, current_queue_len);
1507 if batch_size == 0 {
1508 break; }
1510 let mut batch_vec =
1512 q.drain(current_queue_len.saturating_sub(batch_size)..).collect::<Vec<_>>();
1513 batch_vec.reverse(); queue_len.fetch_sub(batch_vec.len(), Ordering::SeqCst);
1516 batch_vec
1517 };
1518
1519 let batch_futures: Vec<_> = batch
1521 .into_iter()
1522 .map(|(name, dep, resource_type, variant_hash)| {
1523 let graph_clone = Arc::clone(&graph);
1525 let all_deps_clone = Arc::clone(&all_deps);
1526 let processed_clone = Arc::clone(&processed);
1527 let queue_clone = Arc::clone(&queue);
1528 let queue_len_clone = Arc::clone(&queue_len);
1529 let pattern_alias_map_clone = Arc::clone(pattern_alias_map);
1530 let progress_clone = progress.clone();
1531 let counter_clone = Arc::clone(&completed_counter);
1532 let prepared_versions_clone = Arc::clone(prepared_versions);
1533 let dependency_map_clone = ctx_dependency_map;
1534 let custom_names_clone = ctx_custom_names;
1535 let manifest_overrides_clone = ctx_manifest_overrides;
1536 let canonical_path_index_clone = Arc::clone(&canonical_path_index);
1537
1538 async move {
1539 let resource_type = resource_type
1540 .expect("resource_type should always be threaded through queue");
1541
1542 let ctx = TransitiveProcessingContext {
1544 input: TransitiveInput {
1545 name,
1546 dep,
1547 resource_type,
1548 variant_hash,
1549 },
1550 shared: TransitiveSharedState {
1551 graph: graph_clone,
1552 all_deps: all_deps_clone,
1553 processed: processed_clone,
1554 queue: queue_clone,
1555 queue_len: queue_len_clone,
1556 pattern_alias_map: pattern_alias_map_clone,
1557 completed_counter: counter_clone,
1558 dependency_map: dependency_map_clone,
1559 custom_names: custom_names_clone,
1560 prepared_versions: &prepared_versions_clone,
1561 canonical_path_index: canonical_path_index_clone,
1562 },
1563 resolution: TransitiveResolutionContext {
1564 ctx_base,
1565 manifest_overrides: manifest_overrides_clone,
1566 core,
1567 services,
1568 },
1569 progress: progress_clone,
1570 };
1571
1572 process_single_transitive_dependency(ctx).await
1573 }
1574 })
1575 .collect();
1576
1577 let timeout_duration = batch_operation_timeout();
1579 let results = tokio::time::timeout(timeout_duration, join_all(batch_futures))
1580 .await
1581 .with_context(|| {
1582 format!(
1583 "Batch transitive resolution timed out after {:?} - possible deadlock",
1584 timeout_duration
1585 )
1586 })?;
1587
1588 for result in results {
1590 result?;
1591 }
1592 }
1593
1594 acquire_mutex_with_timeout(&graph, "dependency_graph").await?.detect_cycles()?;
1596
1597 let ordered_nodes =
1599 acquire_mutex_with_timeout(&graph, "dependency_graph").await?.topological_order()?;
1600
1601 build_ordered_result(all_deps, ordered_nodes)
1603}
1604
1605fn create_skill_md_dependency(dep: &ResourceDependency) -> ResourceDependency {
1611 match dep {
1612 ResourceDependency::Simple(path) => {
1613 let skill_md_path = format!("{}/SKILL.md", path.trim_end_matches('/'));
1615 ResourceDependency::Simple(skill_md_path)
1616 }
1617 ResourceDependency::Detailed(detailed) => {
1618 let skill_md_path = format!("{}/SKILL.md", detailed.path.trim_end_matches('/'));
1620 ResourceDependency::Detailed(Box::new(DetailedDependency {
1621 path: skill_md_path,
1622 source: detailed.source.clone(),
1623 version: detailed.version.clone(),
1624 branch: detailed.branch.clone(),
1625 rev: detailed.rev.clone(),
1626 command: detailed.command.clone(),
1627 args: detailed.args.clone(),
1628 target: detailed.target.clone(),
1629 filename: detailed.filename.clone(),
1630 dependencies: detailed.dependencies.clone(),
1631 tool: detailed.tool.clone(),
1632 flatten: detailed.flatten,
1633 install: detailed.install,
1634 template_vars: detailed.template_vars.clone(),
1635 }))
1636 }
1637 }
1638}