1use anyhow::{Context as _, Result};
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::path::Path;
9use std::str::FromStr;
10
11use crate::core::ResourceType;
12use crate::lockfile::lockfile_dependency_ref::LockfileDependencyRef;
13use crate::lockfile::{LockedResource, ResourceId};
14
15use super::extractors::{DependencyExtractor, create_dependency_ref_string};
16use crate::templating::cache::RenderCacheKey;
17use crate::templating::content::{
18 NON_TEMPLATED_LITERAL_GUARD_START, content_contains_template_syntax,
19};
20use crate::templating::context::DependencyData;
21use crate::templating::renderer::TemplateRenderer;
22use crate::templating::utils::to_native_path_display;
23
24pub(crate) async fn build_dependencies_data<T: DependencyExtractor>(
40 extractor: &T,
41 current_resource: &LockedResource,
42 rendering_stack: &mut HashSet<String>,
43) -> Result<BTreeMap<String, BTreeMap<String, DependencyData>>> {
44 let mut deps = BTreeMap::new();
45
46 let dependency_specs =
49 extractor.extract_dependency_specs(current_resource).await.with_context(|| {
50 format!(
51 "Failed to extract dependency specifications from resource '{}' (type: {:?})",
52 current_resource.name, current_resource.resource_type
53 )
54 })?;
55
56 let get_key_names =
58 |resource: &LockedResource, dep_type: &ResourceType| -> (String, String, String, String) {
59 let type_str_plural = dep_type.to_plural().to_string();
60 let type_str_singular = dep_type.to_string();
61
62 let key_name = if resource.name.contains('/') || resource.name.contains('\\') {
66 std::path::Path::new(&resource.name)
68 .file_stem()
69 .and_then(|s| s.to_str())
70 .unwrap_or(&resource.name)
71 .to_string()
72 } else {
73 resource.name.clone()
75 };
76
77 let sanitized_key = key_name.replace('-', "_");
80
81 (type_str_plural, type_str_singular, key_name, sanitized_key)
82 };
83
84 let mut resources_to_process: Vec<(&LockedResource, ResourceType, bool)> = Vec::new();
87 let mut visited_dep_ids = HashSet::new();
88
89 for dep_ref in current_resource.parsed_dependencies() {
90 let dep_id = dep_ref.to_string();
92
93 if !visited_dep_ids.insert(dep_id.clone()) {
95 continue;
96 }
97
98 let resource_type = dep_ref.resource_type;
99 let name = &dep_ref.path;
100
101 let dep_spec = {
106 let normalized_path = {
108 let path = std::path::Path::new(&dep_ref.path);
109 let normalized = crate::utils::normalize_path(path);
110 normalized.to_string_lossy().to_string()
111 };
112
113 let normalized_dep_ref = LockfileDependencyRef::new(
115 dep_ref.source.clone(),
116 dep_ref.resource_type,
117 normalized_path,
118 dep_ref.version.clone(),
119 );
120 let normalized_dep_id = normalized_dep_ref.to_string();
121
122 dependency_specs.get(&normalized_dep_id)
123 };
124
125 tracing::debug!(
126 "Looking up dep_spec for dep_id='{}', found={}, available_keys={:?}",
127 dep_id,
128 dep_spec.is_some(),
129 dependency_specs.keys().collect::<Vec<_>>()
130 );
131
132 let dep_tool =
135 dep_spec.and_then(|spec| spec.tool.as_ref()).or(current_resource.tool.as_ref());
136
137 let dep_source = dep_ref.source.as_ref().or(current_resource.source.as_ref());
140
141 let dep_resource_id_with_parent_hash = ResourceId::new(
144 name.clone(),
145 dep_source.cloned(),
146 dep_tool.cloned(),
147 resource_type,
148 current_resource.variant_inputs.hash().to_string(),
149 );
150
151 tracing::debug!(
152 "[DEBUG] Template context looking up: name='{}', type={:?}, source={:?}, tool={:?}, hash={}",
153 name,
154 resource_type,
155 dep_source,
156 dep_tool,
157 ¤t_resource.variant_inputs.hash().to_string()[..8]
158 );
159
160 let mut dep_resource =
163 extractor.lockfile().find_resource_by_id(&dep_resource_id_with_parent_hash);
164
165 if dep_resource.is_none() {
167 let dep_resource_id_empty_hash = ResourceId::new(
168 name.clone(),
169 dep_source.cloned(),
170 dep_tool.cloned(),
171 resource_type,
172 crate::resolver::lockfile_builder::VariantInputs::default().hash().to_string(),
173 );
174 dep_resource = extractor.lockfile().find_resource_by_id(&dep_resource_id_empty_hash);
175
176 if dep_resource.is_some() {
177 tracing::debug!(
178 " [DIRECT MANIFEST DEP] Found dependency '{}' with empty variant_hash (direct manifest dependency)",
179 name
180 );
181 }
182 }
183
184 if let Some(dep_resource) = dep_resource {
185 resources_to_process.push((dep_resource, resource_type, true));
187
188 tracing::debug!(
189 " [DIRECT DEP] Found dependency '{}' (tool: {:?}) for '{}'",
190 name,
191 dep_tool,
192 current_resource.name
193 );
194 } else {
195 tracing::warn!(
196 "Dependency '{}' (type: {:?}, tool: {:?}) not found in lockfile for resource '{}'",
197 name,
198 resource_type,
199 dep_tool,
200 current_resource.name
201 );
202 }
203 }
204
205 tracing::debug!(
206 "Building dependencies data with {} direct dependencies for '{}'",
207 resources_to_process.len(),
208 current_resource.name
209 );
210
211 resources_to_process.sort_by(|a, b| {
216 use std::cmp::Ordering;
217 match a.1.cmp(&b.1) {
219 Ordering::Equal => {
220 match a.0.name.cmp(&b.0.name) {
222 Ordering::Equal => {
223 b.2.cmp(&a.2) }
226 other => other,
227 }
228 }
229 other => other,
230 }
231 });
232
233 for (resource, dep_type, is_dep) in &resources_to_process {
235 tracing::debug!(
236 " [LOCKFILE] Resource: {} (type: {:?}, install: {:?}, is_dependency: {})",
237 resource.name,
238 dep_type,
239 resource.install,
240 is_dep
241 );
242 }
243
244 let current_resource_id = create_dependency_ref_string(
246 current_resource.source.clone(),
247 current_resource.resource_type,
248 current_resource.name.clone(),
249 current_resource.version.clone(),
250 );
251
252 for (resource, dep_type, is_dependency) in &resources_to_process {
254 let resource_id = create_dependency_ref_string(
255 resource.source.clone(),
256 *dep_type,
257 resource.name.clone(),
258 resource.version.clone(),
259 );
260
261 if resource_id == current_resource_id {
263 tracing::debug!(
264 " Skipping current resource: {} (preventing self-reference)",
265 resource.name
266 );
267 continue;
268 }
269
270 tracing::debug!(" Processing resource: {} ({})", resource.name, dep_type);
271
272 let (type_str_plural, type_str_singular, _key_name, sanitized_key) =
273 get_key_names(resource, dep_type);
274
275 let raw_content = extractor.extract_content(resource).await;
279
280 let should_render = if *is_dependency {
283 if let Some(content) = &raw_content {
284 if content.contains(NON_TEMPLATED_LITERAL_GUARD_START) {
286 false
287 } else {
288 content_contains_template_syntax(content)
290 }
291 } else {
292 false
293 }
294 } else {
295 false
297 };
298
299 let final_content: String = if should_render {
301 let cache_key = RenderCacheKey::new(
306 resource.path.clone(),
307 *dep_type,
308 resource.tool.clone(),
309 resource.variant_inputs.hash().to_string(),
310 resource.resolved_commit.clone(),
311 );
312
313 let cache_result = extractor
315 .render_cache()
316 .lock()
317 .map_err(|e| {
318 anyhow::anyhow!(
319 "Render cache lock poisoned for resource '{}': {}. \
320 This indicates a panic occurred while holding the lock.",
321 resource.name,
322 e
323 )
324 })?
325 .get(&cache_key)
326 .cloned(); if let Some(cached_content) = cache_result {
329 tracing::debug!("Render cache hit for '{}' ({})", resource.name, dep_type);
330 cached_content
331 } else {
332 tracing::debug!(
334 "Render cache miss for '{}' ({}), rendering...",
335 resource.name,
336 dep_type
337 );
338
339 let dep_id = create_dependency_ref_string(
341 resource.source.clone(),
342 *dep_type,
343 resource.name.clone(),
344 resource.version.clone(),
345 );
346 if rendering_stack.contains(&dep_id) {
347 let chain: Vec<String> = rendering_stack.iter().cloned().collect();
348 anyhow::bail!(
349 "Circular dependency detected while rendering '{}'. \
350 Dependency chain: {} -> {}",
351 resource.name,
352 chain.join(" -> "),
353 dep_id
354 );
355 }
356
357 rendering_stack.insert(dep_id.clone());
359
360 let dep_resource_id = ResourceId::from_resource(resource);
362 let render_result = Box::pin(extractor.build_context_with_visited(
363 &dep_resource_id,
364 resource.variant_inputs.json(),
365 rendering_stack,
366 ))
367 .await;
368
369 rendering_stack.remove(&dep_id);
371
372 match render_result {
373 Ok(dep_context) => {
374 if let Some(content) = raw_content {
376 let mut renderer = TemplateRenderer::new(
377 true,
378 extractor.project_dir().clone(),
379 None,
380 )
381 .with_context(|| {
382 format!(
383 "Failed to create template renderer for dependency '{}' (type: {:?})",
384 resource.name,
385 dep_type
386 )
387 })?;
388
389 let metadata = crate::templating::renderer::RenderingMetadata {
391 resource_name: resource.name.clone(),
392 resource_type: *dep_type,
393 dependency_chain: vec![], source_path: None,
395 depth: rendering_stack.len(),
396 };
397
398 let rendered = renderer
399 .render_template(&content, &dep_context, Some(&metadata))
400 .with_context(|| {
401 format!(
402 "Failed to render dependency '{}' (type: {:?}). \
403 This is a HARD FAILURE - dependency content MUST render successfully.\n\
404 Resource: {} (source: {}, path: {})",
405 resource.name,
406 dep_type,
407 resource.name,
408 resource.source.as_deref().unwrap_or("local"),
409 resource.path
410 )
411 })?;
412
413 tracing::debug!(
414 "Successfully rendered dependency content for '{}'",
415 resource.name
416 );
417
418 if let Ok(mut cache) = extractor.render_cache().lock() {
420 cache.insert(cache_key.clone(), rendered.clone());
421 tracing::debug!(
422 "Stored rendered content in cache for '{}'",
423 resource.name
424 );
425 }
426
427 rendered
428 } else {
429 String::new()
431 }
432 }
433 Err(e) => {
434 return Err(e.context(format!(
436 "Failed to build template context for dependency '{}' (type: {:?}). \
437 This is a HARD FAILURE - all dependencies must have valid contexts.\n\
438 Resource: {} (source: {}, path: {})",
439 resource.name,
440 dep_type,
441 resource.name,
442 resource.source.as_deref().unwrap_or("local"),
443 resource.path
444 )));
445 }
446 }
447 }
448 } else {
449 raw_content.unwrap_or_default()
451 };
452
453 let dependency_data = DependencyData {
455 resource_type: type_str_singular,
456 name: resource.name.clone(),
457 install_path: to_native_path_display(&resource.installed_at),
458 source: resource.source.clone(),
459 version: resource.version.clone(),
460 resolved_commit: resource.resolved_commit.clone(),
461 checksum: resource.checksum.clone(),
462 path: resource.path.clone(),
463 content: final_content,
464 };
465
466 let type_deps: &mut BTreeMap<String, DependencyData> =
468 deps.entry(type_str_plural.clone()).or_insert_with(BTreeMap::new);
469 type_deps.insert(sanitized_key.clone(), dependency_data);
470
471 tracing::debug!(
472 " Added resource: {}[{}] -> {}",
473 type_str_plural,
474 sanitized_key,
475 resource.path
476 );
477 }
478
479 tracing::debug!(
482 "Extracting custom dependency names for direct deps of: '{}'",
483 current_resource.name
484 );
485
486 let current_custom_names =
488 extractor.extract_dependency_custom_names(current_resource).await.with_context(|| {
489 format!(
490 "Failed to extract custom dependency names from resource '{}' (type: {:?})",
491 current_resource.name, current_resource.resource_type
492 )
493 })?;
494 tracing::debug!(
495 "Extracted {} custom names from current resource '{}' (type: {:?})",
496 current_custom_names.len(),
497 current_resource.name,
498 current_resource.resource_type
499 );
500 if !current_custom_names.is_empty() || current_resource.name.contains("golang") {
501 tracing::info!(
502 "Extracted {} custom names from current resource '{}' (type: {:?})",
503 current_custom_names.len(),
504 current_resource.name,
505 current_resource.resource_type
506 );
507 for (dep_ref, custom_name) in ¤t_custom_names {
508 tracing::info!(" Will add alias: '{}' -> '{}'", dep_ref, custom_name);
509 }
510 }
511 for (dep_ref, custom_name) in current_custom_names {
512 add_custom_alias(&mut deps, &dep_ref, &custom_name);
513 }
514
515 tracing::debug!(
517 "Built dependencies data with {} resource types for '{}'",
518 deps.len(),
519 current_resource.name
520 );
521 for (resource_type, resources) in &deps {
522 tracing::debug!(" Type {}: {} resources", resource_type, resources.len());
523 if resource_type == "snippets" {
524 for (key, data) in resources {
525 tracing::debug!(" - key='{}', name='{}', path='{}'", key, data.name, data.path);
526 }
527 } else {
528 for name in resources.keys() {
529 tracing::debug!(" - {}", name);
530 }
531 }
532 }
533
534 Ok(deps)
535}
536
537pub(crate) fn add_custom_alias(
547 deps: &mut BTreeMap<String, BTreeMap<String, DependencyData>>,
548 dep_ref: &str,
549 custom_name: &str,
550) {
551 let dep_ref_parsed = match LockfileDependencyRef::from_str(dep_ref) {
553 Ok(dep_ref) => dep_ref,
554 Err(e) => {
555 tracing::debug!(
556 "Skipping invalid dep_ref format '{}' for custom name '{}': {}",
557 dep_ref,
558 custom_name,
559 e
560 );
561 return;
562 }
563 };
564
565 let dep_type = dep_ref_parsed.resource_type;
566 let dep_name = &dep_ref_parsed.path;
567
568 let type_str_plural = dep_type.to_plural().to_string();
569
570 if let Some(type_deps) = deps.get_mut(&type_str_plural) {
572 let name_to_key: HashMap<String, String> = type_deps
574 .iter()
575 .flat_map(|(key, data)| {
576 let mut mappings = vec![(data.name.clone(), key.clone())];
578
579 if let Some(basename) = Path::new(&data.name).file_name().and_then(|n| n.to_str()) {
581 mappings.push((basename.to_string(), key.clone()));
582 }
583 if let Some(stem) = Path::new(&data.path).file_stem().and_then(|n| n.to_str()) {
584 mappings.push((stem.to_string(), key.clone()));
585 }
586 if let Some(path_basename) =
587 Path::new(&data.path).file_name().and_then(|n| n.to_str())
588 {
589 mappings.push((path_basename.to_string(), key.clone()));
590 }
591
592 mappings
593 })
594 .collect();
595
596 let existing_data =
598 name_to_key.get(dep_name).and_then(|key| type_deps.get(key).cloned()).or_else(|| {
599 Path::new(dep_name)
603 .file_name()
604 .and_then(|name| name.to_str())
605 .and_then(|basename| name_to_key.get(basename))
606 .and_then(|key| type_deps.get(key).cloned())
607 });
608
609 if let Some(data) = existing_data {
610 let sanitized_alias = custom_name.replace('-', "_");
612
613 tracing::info!(
614 "ā Added {} alias '{}' -> resource '{}' (path: {})",
615 type_str_plural,
616 sanitized_alias,
617 dep_name,
618 data.path
619 );
620
621 type_deps.insert(sanitized_alias.clone(), data);
623 } else {
624 tracing::error!(
625 "ā NOT FOUND: {} resource '{}' for alias '{}'.\n \
626 Dep ref: '{}'\n \
627 Available {} (first 5): {}",
628 type_str_plural,
629 dep_name,
630 custom_name,
631 dep_ref,
632 type_deps.len(),
633 type_deps
634 .iter()
635 .take(5)
636 .map(|(k, v)| format!("'{}' (name='{}')", k, v.name))
637 .collect::<Vec<_>>()
638 .join(", ")
639 );
640 }
641 } else {
642 tracing::debug!(
643 "Resource type '{}' not found in deps map when adding custom alias '{}' for '{}'",
644 type_str_plural,
645 custom_name,
646 dep_ref
647 );
648 }
649}