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