agpm_cli/installer/context.rs
1//! Installation context and helper utilities.
2//!
3//! This module provides the [`InstallContext`] type and its builder for managing
4//! installation parameters throughout the AGPM installation pipeline.
5//!
6//! # Cross-Process Safety
7//!
8//! Cross-process coordination is handled at the command level via `ProjectLock`.
9//! This context no longer carries mutex fields.
10//!
11//! # Examples
12//!
13//! Basic usage with the builder pattern:
14//!
15//! ```rust,no_run
16//! use agpm_cli::installer::InstallContext;
17//! use agpm_cli::cache::Cache;
18//! use std::path::Path;
19//!
20//! # async fn example() -> anyhow::Result<()> {
21//! let project_dir = Path::new(".");
22//! let cache = Cache::new()?;
23//!
24//! // Create a basic context
25//! let context = InstallContext::builder(&project_dir, &cache)
26//! .force_refresh(true)
27//! .verbose(false)
28//! .build();
29//!
30//! // With manifest and lockfile
31//! # use agpm_cli::manifest::Manifest;
32//! # use agpm_cli::lockfile::LockFile;
33//! # use std::sync::Arc;
34//! # let manifest = Manifest::default();
35//! # let lockfile = Arc::new(LockFile::default());
36//! let context = InstallContext::builder(&project_dir, &cache)
37//! .manifest(&manifest)
38//! .lockfile(&lockfile)
39//! .force_refresh(false)
40//! .build();
41//! # Ok(())
42//! # }
43//! ```
44
45use std::path::Path;
46use std::sync::Arc;
47
48use crate::cache::Cache;
49use crate::lockfile::LockFile;
50use crate::manifest::Manifest;
51
52/// Installation context containing common parameters for resource installation.
53///
54/// This struct bundles frequently-used installation parameters to reduce
55/// function parameter counts and improve code readability. It's used throughout
56/// the installation pipeline to pass configuration and context information.
57///
58/// # Fields
59///
60/// * `project_dir` - Root directory of the project where resources will be installed
61/// * `cache` - Cache instance for managing Git repositories and worktrees
62/// * `force_refresh` - Whether to force refresh of cached worktrees
63/// * `manifest` - Optional reference to the project manifest for template context
64/// * `lockfile` - Optional reference to the lockfile for template context
65/// * `old_lockfile` - Optional reference to the previous lockfile for early-exit optimization
66/// * `project_patches` - Optional project-level patches from agpm.toml
67/// * `private_patches` - Optional user-level patches from agpm.private.toml
68pub struct InstallContext<'a> {
69 pub project_dir: &'a Path,
70 pub cache: &'a Cache,
71 pub force_refresh: bool,
72 pub verbose: bool,
73 pub manifest: Option<&'a Manifest>,
74 pub lockfile: Option<&'a Arc<LockFile>>,
75 pub old_lockfile: Option<&'a LockFile>,
76 pub project_patches: Option<&'a crate::manifest::ManifestPatches>,
77 pub private_patches: Option<&'a crate::manifest::ManifestPatches>,
78 pub max_content_file_size: Option<u64>,
79 /// Shared template context builder for all resources
80 pub template_context_builder: Arc<crate::templating::TemplateContextBuilder>,
81 /// Trust lockfile checksums without recomputing (ultra-fast path optimization).
82 ///
83 /// When enabled and all inputs match the old lockfile entry, skip file I/O and
84 /// return the stored checksum. Safe for immutable dependencies (tags/SHAs).
85 ///
86 /// See module-level docs in [`crate::cli::install`] for optimization tier details.
87 pub trust_lockfile_checksums: bool,
88}
89
90/// Builder for creating InstallContext instances with a fluent API.
91pub struct InstallContextBuilder<'a> {
92 // Required parameters
93 project_dir: &'a Path,
94 cache: &'a Cache,
95
96 // Optional with sensible defaults
97 force_refresh: bool,
98 verbose: bool,
99 trust_lockfile_checksums: bool,
100
101 // Truly optional parameters
102 manifest: Option<&'a Manifest>,
103 lockfile: Option<&'a Arc<LockFile>>,
104 old_lockfile: Option<&'a LockFile>,
105 project_patches: Option<&'a crate::manifest::ManifestPatches>,
106 private_patches: Option<&'a crate::manifest::ManifestPatches>,
107 max_content_file_size: Option<u64>,
108}
109
110impl<'a> InstallContextBuilder<'a> {
111 /// Create a new builder with required parameters.
112 pub fn new(project_dir: &'a Path, cache: &'a Cache) -> Self {
113 Self {
114 project_dir,
115 cache,
116 force_refresh: false,
117 verbose: false,
118 trust_lockfile_checksums: false,
119 manifest: None,
120 lockfile: None,
121 old_lockfile: None,
122 project_patches: None,
123 private_patches: None,
124 max_content_file_size: None,
125 }
126 }
127
128 /// Set whether to force refresh of cached worktrees.
129 pub fn force_refresh(mut self, value: bool) -> Self {
130 self.force_refresh = value;
131 self
132 }
133
134 /// Set verbose output.
135 pub fn verbose(mut self, value: bool) -> Self {
136 self.verbose = value;
137 self
138 }
139
140 /// Trust lockfile checksums without recomputing (fast path optimization).
141 ///
142 /// When enabled, if a file exists and all inputs match the old lockfile,
143 /// we return the stored checksum without reading/hashing the file.
144 pub fn trust_lockfile_checksums(mut self, value: bool) -> Self {
145 self.trust_lockfile_checksums = value;
146 self
147 }
148
149 /// Set the project manifest for template context.
150 pub fn manifest(mut self, manifest: &'a Manifest) -> Self {
151 self.manifest = Some(manifest);
152 self
153 }
154
155 /// Set the lockfile for template context.
156 pub fn lockfile(mut self, lockfile: &'a Arc<LockFile>) -> Self {
157 self.lockfile = Some(lockfile);
158 self
159 }
160
161 /// Set the previous lockfile for early-exit optimization.
162 pub fn old_lockfile(mut self, old_lockfile: &'a LockFile) -> Self {
163 self.old_lockfile = Some(old_lockfile);
164 self
165 }
166
167 /// Set project-level patches from agpm.toml.
168 pub fn project_patches(mut self, patches: &'a crate::manifest::ManifestPatches) -> Self {
169 self.project_patches = Some(patches);
170 self
171 }
172
173 /// Set user-level patches from agpm.private.toml.
174 pub fn private_patches(mut self, patches: &'a crate::manifest::ManifestPatches) -> Self {
175 self.private_patches = Some(patches);
176 self
177 }
178
179 /// Set maximum content file size for embedding.
180 pub fn max_content_file_size(mut self, size: u64) -> Self {
181 self.max_content_file_size = Some(size);
182 self
183 }
184
185 /// Set commonly used options in a single call.
186 ///
187 /// This method groups frequently used options to reduce the number of
188 /// builder method calls in common installation scenarios.
189 ///
190 /// # Arguments
191 ///
192 /// * `force_refresh` - Whether to force refresh cached worktrees
193 /// * `verbose` - Whether to enable verbose output
194 /// * `manifest` - Optional project manifest
195 /// * `lockfile` - Optional lockfile for template context
196 pub fn with_common_options(
197 mut self,
198 force_refresh: bool,
199 verbose: bool,
200 manifest: Option<&'a Manifest>,
201 lockfile: Option<&'a Arc<LockFile>>,
202 ) -> Self {
203 self.force_refresh = force_refresh;
204 self.verbose = verbose;
205 self.manifest = manifest;
206 self.lockfile = lockfile;
207 self
208 }
209
210 /// Build the InstallContext with the configured parameters.
211 #[must_use] // The context is needed for installation, ignoring it defeats the purpose
212 pub fn build(self) -> InstallContext<'a> {
213 // Create shared template context builder
214 // Use lockfile if available, otherwise create with empty lockfile
215 let (lockfile_for_builder, project_config) = if let Some(lf) = self.lockfile {
216 (lf.clone(), self.manifest.and_then(|m| m.project.clone()))
217 } else {
218 // No lockfile - create an empty one for the builder
219 (Arc::new(LockFile::default()), None)
220 };
221
222 // Clone cache and wrap in Arc for TemplateContextBuilder
223 // The clone is necessary because we have &Cache but need Arc<Cache>
224 // Cache cloning is relatively cheap (Arc'd internals) and only happens once per installation
225 let template_context_builder = Arc::new(crate::templating::TemplateContextBuilder::new(
226 lockfile_for_builder,
227 project_config,
228 Arc::new(self.cache.clone()),
229 self.project_dir.to_path_buf(),
230 ));
231
232 InstallContext {
233 project_dir: self.project_dir,
234 cache: self.cache,
235 force_refresh: self.force_refresh,
236 verbose: self.verbose,
237 manifest: self.manifest,
238 lockfile: self.lockfile,
239 old_lockfile: self.old_lockfile,
240 project_patches: self.project_patches,
241 private_patches: self.private_patches,
242 max_content_file_size: self.max_content_file_size,
243 template_context_builder,
244 trust_lockfile_checksums: self.trust_lockfile_checksums,
245 }
246 }
247}
248
249impl<'a> InstallContext<'a> {
250 /// Create a new builder for InstallContext.
251 pub fn builder(project_dir: &'a Path, cache: &'a Cache) -> InstallContextBuilder<'a> {
252 InstallContextBuilder::new(project_dir, cache)
253 }
254
255 /// Create an InstallContext with common options for parallel installation.
256 ///
257 /// This helper function reduces code duplication by handling the common pattern
258 /// of setting up InstallContext with frequently used options.
259 ///
260 /// # Arguments
261 ///
262 /// * `project_dir` - Root directory of the project
263 /// * `cache` - Cache instance for managing Git repositories
264 /// * `manifest` - Optional project manifest
265 /// * `lockfile` - Lockfile for template context
266 /// * `force_refresh` - Whether to force refresh cached worktrees
267 /// * `verbose` - Whether to enable verbose output
268 /// * `old_lockfile` - Optional previous lockfile for early-exit optimization
269 pub fn with_common_options(
270 project_dir: &'a Path,
271 cache: &'a Cache,
272 manifest: Option<&'a Manifest>,
273 lockfile: Option<&'a Arc<LockFile>>,
274 force_refresh: bool,
275 verbose: bool,
276 old_lockfile: Option<&'a LockFile>,
277 ) -> Self {
278 Self::with_common_options_and_trust(
279 project_dir,
280 cache,
281 manifest,
282 lockfile,
283 force_refresh,
284 verbose,
285 old_lockfile,
286 false, // trust_lockfile_checksums defaults to false
287 )
288 }
289
290 /// Create an InstallContext with common options including trust flag.
291 ///
292 /// This is the full version that allows specifying `trust_lockfile_checksums`.
293 #[allow(clippy::too_many_arguments)]
294 pub fn with_common_options_and_trust(
295 project_dir: &'a Path,
296 cache: &'a Cache,
297 manifest: Option<&'a Manifest>,
298 lockfile: Option<&'a Arc<LockFile>>,
299 force_refresh: bool,
300 verbose: bool,
301 old_lockfile: Option<&'a LockFile>,
302 trust_lockfile_checksums: bool,
303 ) -> Self {
304 let mut builder = Self::builder(project_dir, cache)
305 .force_refresh(force_refresh)
306 .verbose(verbose)
307 .trust_lockfile_checksums(trust_lockfile_checksums);
308
309 // Add optional fields only if present
310 if let Some(m) = manifest {
311 builder = builder.manifest(m);
312 // Add patches from manifest if available
313 if !m.project_patches.is_empty() {
314 builder = builder.project_patches(&m.project_patches);
315 }
316 if !m.private_patches.is_empty() {
317 builder = builder.private_patches(&m.private_patches);
318 }
319 }
320
321 if let Some(lf) = lockfile {
322 builder = builder.lockfile(lf);
323 }
324
325 if let Some(old_lf) = old_lockfile {
326 builder = builder.old_lockfile(old_lf);
327 }
328
329 builder.build()
330 }
331}