agpm_cli/installer/
context.rs

1//! Installation context and helper utilities.
2
3use anyhow::Result;
4use std::path::Path;
5use std::sync::Arc;
6use std::time::Duration;
7use tokio::sync::Mutex;
8
9use crate::cache::Cache;
10use crate::lockfile::LockFile;
11use crate::manifest::Manifest;
12
13/// Installation context containing common parameters for resource installation.
14///
15/// This struct bundles frequently-used installation parameters to reduce
16/// function parameter counts and improve code readability. It's used throughout
17/// the installation pipeline to pass configuration and context information.
18///
19/// # Fields
20///
21/// * `project_dir` - Root directory of the project where resources will be installed
22/// * `cache` - Cache instance for managing Git repositories and worktrees
23/// * `force_refresh` - Whether to force refresh of cached worktrees
24/// * `manifest` - Optional reference to the project manifest for template context
25/// * `lockfile` - Optional reference to the lockfile for template context
26/// * `old_lockfile` - Optional reference to the previous lockfile for early-exit optimization
27/// * `project_patches` - Optional project-level patches from agpm.toml
28/// * `private_patches` - Optional user-level patches from agpm.private.toml
29pub struct InstallContext<'a> {
30    pub project_dir: &'a Path,
31    pub cache: &'a Cache,
32    pub force_refresh: bool,
33    pub verbose: bool,
34    pub manifest: Option<&'a Manifest>,
35    pub lockfile: Option<&'a Arc<LockFile>>,
36    pub old_lockfile: Option<&'a LockFile>,
37    pub project_patches: Option<&'a crate::manifest::ManifestPatches>,
38    pub private_patches: Option<&'a crate::manifest::ManifestPatches>,
39    pub gitignore_lock: Option<&'a Arc<Mutex<()>>>,
40    pub max_content_file_size: Option<u64>,
41    /// Shared template context builder for all resources
42    pub template_context_builder: Arc<crate::templating::TemplateContextBuilder>,
43}
44
45impl<'a> InstallContext<'a> {
46    /// Create a new installation context.
47    #[allow(clippy::too_many_arguments)]
48    pub fn new(
49        project_dir: &'a Path,
50        cache: &'a Cache,
51        force_refresh: bool,
52        verbose: bool,
53        manifest: Option<&'a Manifest>,
54        lockfile: Option<&'a Arc<LockFile>>,
55        old_lockfile: Option<&'a LockFile>,
56        project_patches: Option<&'a crate::manifest::ManifestPatches>,
57        private_patches: Option<&'a crate::manifest::ManifestPatches>,
58        gitignore_lock: Option<&'a Arc<Mutex<()>>>,
59        max_content_file_size: Option<u64>,
60    ) -> Self {
61        // Create shared template context builder
62        // Use lockfile if available, otherwise create with empty lockfile
63        let (lockfile_for_builder, project_config) = if let Some(lf) = lockfile {
64            (lf.clone(), manifest.and_then(|m| m.project.clone()))
65        } else {
66            // No lockfile - create an empty one for the builder
67            (Arc::new(LockFile::default()), None)
68        };
69
70        let template_context_builder = Arc::new(crate::templating::TemplateContextBuilder::new(
71            lockfile_for_builder,
72            project_config,
73            Arc::new(cache.clone()),
74            project_dir.to_path_buf(),
75        ));
76
77        Self {
78            project_dir,
79            cache,
80            force_refresh,
81            verbose,
82            manifest,
83            lockfile,
84            old_lockfile,
85            project_patches,
86            private_patches,
87            gitignore_lock,
88            max_content_file_size,
89            template_context_builder,
90        }
91    }
92}
93
94/// Read a file with retry logic to handle cross-process filesystem cache coherency issues.
95///
96/// This function wraps `tokio::fs::read_to_string` with retry logic to handle cases where
97/// files created by Git subprocesses are not immediately visible to the parent Rust process
98/// due to filesystem cache propagation delays. This is particularly important in CI
99/// environments with network-attached storage where cache coherency delays can be significant.
100///
101/// # Arguments
102///
103/// * `path` - The file path to read
104///
105/// # Returns
106///
107/// Returns the file content as a `String`, or an error if the file cannot be read after retries.
108///
109/// # Retry Strategy
110///
111/// - Initial delay: 10ms
112/// - Max delay: 500ms
113/// - Factor: 2x (exponential backoff)
114/// - Max attempts: 10
115/// - Total max time: ~10 seconds
116///
117/// Only `NotFound` errors are retried, as these indicate cache coherency issues.
118/// Other errors (permissions, I/O errors) fail immediately by returning Ok to bypass retry.
119pub(crate) async fn read_with_cache_retry(path: &Path) -> Result<String> {
120    use std::io;
121
122    let retry_strategy = tokio_retry::strategy::ExponentialBackoff::from_millis(10)
123        .max_delay(Duration::from_millis(500))
124        .factor(2)
125        .take(10);
126
127    let path_buf = path.to_path_buf();
128
129    tokio_retry::Retry::spawn(retry_strategy, || {
130        let path = path_buf.clone();
131        async move {
132            tokio::fs::read_to_string(&path).await.map_err(|e| {
133                if e.kind() == io::ErrorKind::NotFound {
134                    tracing::debug!(
135                        "File not yet visible (likely cache coherency issue): {}",
136                        path.display()
137                    );
138                    format!("File not found: {}", path.display())
139                } else {
140                    // Non-retriable error - return error message that will fail fast
141                    format!("I/O error (non-retriable): {}", e)
142                }
143            })
144        }
145    })
146    .await
147    .map_err(|e| anyhow::anyhow!("Failed to read resource file: {}: {}", path.display(), e))
148}