pub mod backtracking;
pub mod conflict_service;
pub mod dependency_graph;
mod dependency_processing;
mod entry_builder;
mod incremental_update;
pub mod lockfile_builder;
pub mod path_resolver;
pub mod pattern_expander;
pub mod resource_service;
pub mod sha_conflict_detector;
pub mod skills;
pub mod source_context;
pub mod transitive_extractor;
pub mod transitive_resolver;
pub mod types;
pub mod version_resolver;
#[cfg(test)]
mod tests;
pub use path_resolver::{extract_meaningful_path, is_file_relative_path, normalize_bare_filename};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use anyhow::{Context, Result};
use dashmap::DashMap;
use crate::cache::Cache;
use crate::core::{OperationContext, ResourceType};
use crate::lockfile::{LockFile, LockedResource};
use crate::manifest::{Manifest, ResourceDependency};
use crate::source::SourceManager;
pub use conflict_service::ConflictService;
pub use pattern_expander::PatternExpansionService;
pub use resource_service::ResourceFetchingService;
pub use types::ResolutionCore;
pub use version_resolver::{
VersionResolutionService, VersionResolver as VersionResolverExport, find_best_matching_tag,
is_version_constraint, parse_tags_to_versions,
};
pub use dependency_graph::{DependencyGraph, DependencyNode};
pub use lockfile_builder::LockfileBuilder;
pub use pattern_expander::{expand_pattern_to_concrete_deps, generate_dependency_name};
pub use types::{
ConflictDetectionKey, DependencyKey, ManifestOverride, ManifestOverrideIndex, OverrideKey,
ResolutionContext, ResolvedDependenciesMap, ResolvedDependencyInfo, TransitiveContext,
};
pub use version_resolver::{PreparedSourceVersion, VersionResolver, WorktreeManager};
pub struct DependencyResolver {
core: ResolutionCore,
version_service: VersionResolutionService,
pattern_service: PatternExpansionService,
conflict_detector: crate::version::conflict::ConflictDetector,
sha_conflict_detector: crate::resolver::sha_conflict_detector::ShaConflictDetector,
dependency_map: Arc<DashMap<DependencyKey, Vec<String>>>,
pattern_alias_map: Arc<DashMap<(ResourceType, String), String>>,
transitive_custom_names: Arc<DashMap<DependencyKey, String>>,
sources_pre_synced: std::sync::atomic::AtomicBool,
resolved_deps_for_conflict_check: ResolvedDependenciesMap,
reverse_dependency_map: std::sync::Arc<dashmap::DashMap<String, Vec<String>>>,
}
impl DependencyResolver {
fn init_dependencies(
core: ResolutionCore,
version_service: VersionResolutionService,
pattern_service: PatternExpansionService,
) -> Result<Self> {
Ok(Self {
core,
version_service,
pattern_service,
conflict_detector: crate::version::conflict::ConflictDetector::new(),
sha_conflict_detector: crate::resolver::sha_conflict_detector::ShaConflictDetector::new(
),
dependency_map: Arc::new(DashMap::new()),
pattern_alias_map: Arc::new(DashMap::new()),
transitive_custom_names: Arc::new(DashMap::new()),
sources_pre_synced: std::sync::atomic::AtomicBool::new(false),
resolved_deps_for_conflict_check: Arc::new(DashMap::new()),
reverse_dependency_map: std::sync::Arc::new(dashmap::DashMap::new()),
})
}
pub async fn new(manifest: Manifest, cache: Cache) -> Result<Self> {
Self::new_with_context(manifest, cache, None).await
}
pub async fn new_with_context(
manifest: Manifest,
cache: Cache,
operation_context: Option<Arc<OperationContext>>,
) -> Result<Self> {
let source_manager = SourceManager::from_manifest(&manifest)?;
let core = ResolutionCore::new(manifest, cache, source_manager, operation_context);
let version_service = VersionResolutionService::new(core.cache().clone());
let pattern_service = PatternExpansionService::new();
Self::init_dependencies(core, version_service, pattern_service)
}
pub async fn new_with_global(manifest: Manifest, cache: Cache) -> Result<Self> {
Self::new_with_global_concurrency(manifest, cache, None, None).await
}
pub async fn new_with_global_concurrency(
manifest: Manifest,
cache: Cache,
max_concurrency: Option<usize>,
operation_context: Option<Arc<OperationContext>>,
) -> Result<Self> {
let source_manager = SourceManager::from_manifest_with_global(&manifest).await?;
let core = ResolutionCore::new(manifest, cache, source_manager, operation_context);
let version_service = if let Some(concurrency) = max_concurrency {
VersionResolutionService::with_concurrency(core.cache().clone(), concurrency)
} else {
VersionResolutionService::new(core.cache().clone())
};
let pattern_service = PatternExpansionService::new();
Self::init_dependencies(core, version_service, pattern_service)
}
pub async fn with_cache(manifest: Manifest, cache: Cache) -> Result<Self> {
Self::new_with_context(manifest, cache, None).await
}
pub async fn new_with_global_context(
manifest: Manifest,
cache: Cache,
operation_context: Option<Arc<OperationContext>>,
) -> Result<Self> {
Self::new_with_global_concurrency(manifest, cache, None, operation_context).await
}
pub fn core(&self) -> &ResolutionCore {
&self.core
}
pub async fn resolve(&mut self) -> Result<LockFile> {
self.resolve_with_options(true, None).await
}
pub async fn resolve_with_options(
&mut self,
enable_transitive: bool,
progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
) -> Result<LockFile> {
let (base_deps, mut lockfile) = self.prepare_resolution(&progress).await?;
self.pre_sync_sources_if_needed(&base_deps, progress.clone()).await?;
let all_deps = self
.resolve_transitive_dependencies_phase(&base_deps, enable_transitive, progress.clone())
.await?;
self.resolve_individual_dependencies(&all_deps, &mut lockfile, progress.clone()).await?;
self.handle_conflicts_and_backtracking(&mut lockfile).await?;
self.finalize_resolution(&mut lockfile, &progress)?;
Ok(lockfile)
}
async fn prepare_resolution(
&mut self,
progress: &Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
) -> Result<(Vec<(String, ResourceDependency, ResourceType)>, LockFile)> {
self.resolved_deps_for_conflict_check.clear();
self.reverse_dependency_map.clear();
self.conflict_detector = crate::version::conflict::ConflictDetector::new();
let mut lockfile = LockFile::new();
for (name, url) in &self.core.manifest().sources {
lockfile.add_source(name.clone(), url.clone(), String::new());
}
let base_deps: Vec<(String, ResourceDependency, ResourceType)> = self
.core
.manifest()
.all_dependencies_with_types()
.into_iter()
.map(|(name, dep, resource_type)| (name.to_string(), dep.into_owned(), resource_type))
.collect();
let window_size = 7;
if let Some(pm) = progress {
tracing::debug!(
"Starting ResolvingDependencies phase with windowed tracking: {} base deps, {} slots",
base_deps.len(),
window_size
);
pm.start_phase_with_active_tracking(
crate::utils::InstallationPhase::ResolvingDependencies,
base_deps.len(),
window_size,
);
}
Ok((base_deps, lockfile))
}
async fn pre_sync_sources_if_needed(
&mut self,
base_deps: &[(String, ResourceDependency, ResourceType)],
progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
) -> Result<()> {
if !self.sources_pre_synced.load(std::sync::atomic::Ordering::Acquire) {
let deps_for_sync: Vec<(String, ResourceDependency)> =
base_deps.iter().map(|(name, dep, _)| (name.clone(), dep.clone())).collect();
self.version_service.pre_sync_sources(&self.core, &deps_for_sync, progress).await?;
self.sources_pre_synced.store(true, std::sync::atomic::Ordering::Release);
}
Ok(())
}
async fn resolve_transitive_dependencies_phase(
&mut self,
base_deps: &[(String, ResourceDependency, ResourceType)],
enable_transitive: bool,
progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
tracing::info!(
"Phase 3: Starting transitive dependency resolution (enable_transitive={})",
enable_transitive
);
if enable_transitive {
tracing::info!(
"Phase 3: Calling resolve_transitive_dependencies with {} base deps",
base_deps.len()
);
let result = self.resolve_transitive_dependencies(base_deps, progress).await?;
tracing::info!("Phase 3: Resolved {} total deps (including transitive)", result.len());
Ok(result)
} else {
tracing::info!(
"Phase 3: Transitive resolution disabled, using {} base deps",
base_deps.len()
);
Ok(base_deps.to_vec())
}
}
async fn resolve_individual_dependencies(
&mut self,
all_deps: &[(String, ResourceDependency, ResourceType)],
lockfile: &mut LockFile,
progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
) -> Result<()> {
let completed_counter = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let total_deps = all_deps.len();
let cores = std::thread::available_parallelism().map(std::num::NonZero::get).unwrap_or(4);
let max_concurrent = std::cmp::max(10, cores * 2);
let mut all_results = Vec::new();
for chunk in all_deps.chunks(max_concurrent) {
use futures::future::join_all;
let progress_clone = progress.clone();
let batch_futures: Vec<_> = chunk
.iter()
.map(|(name, dep, resource_type)| {
let display_name = if dep.get_source().is_some() {
if let Some(version) = dep.get_version() {
format!("{}@{}", name, version)
} else {
format!("{}@HEAD", name)
}
} else {
name.clone()
};
let progress_key = format!("{}:{}", resource_type, &display_name);
if let Some(pm) = &progress_clone {
pm.mark_item_active(&display_name, &progress_key);
}
let resolution_fut = if dep.is_pattern() {
Box::pin(self.resolve_pattern_dependency(name, dep, *resource_type))
as std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<Vec<LockedResource>>>
+ Send
+ '_,
>,
>
} else {
Box::pin(async {
self.resolve_dependency(name, dep, *resource_type)
.await
.map(|e| vec![e])
})
as std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<Vec<LockedResource>>>
+ Send
+ '_,
>,
>
};
(
resolution_fut,
name.clone(),
dep.clone(),
*resource_type,
progress_key,
display_name,
)
})
.collect();
let timeout_duration = crate::constants::batch_operation_timeout();
let batch_results = tokio::time::timeout(
timeout_duration,
join_all(batch_futures.into_iter().map(
|(fut, name, dep, resource_type, progress_key, display_name)| {
let progress_clone = progress_clone.clone();
let counter_clone = completed_counter.clone();
async move {
let result = fut.await;
if let Some(pm) = &progress_clone {
let completed = counter_clone
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
+ 1;
pm.mark_item_complete(
&progress_key,
Some(&display_name),
completed,
total_deps,
"Resolving dependencies",
);
}
(name, dep, resource_type, result)
}
},
)),
)
.await
.with_context(|| {
format!(
"Batch dependency resolution timed out after {:?} - possible deadlock",
timeout_duration
)
})?;
for (name, dep, resource_type, result) in batch_results {
all_results.push(result.map(|entries| (name, dep, resource_type, entries)));
}
}
for result in all_results {
let (name, dep, resource_type, entries) = result?;
for entry in entries {
self.track_resolved_dependency_for_conflicts(&name, &dep, &entry, resource_type);
self.add_or_update_lockfile_entry(lockfile, entry);
}
}
Ok(())
}
async fn handle_conflicts_and_backtracking(&mut self, lockfile: &mut LockFile) -> Result<()> {
tracing::debug!(
"Phase 5: Processing {} tracked dependencies for conflict detection",
self.resolved_deps_for_conflict_check.len()
);
for entry in self.resolved_deps_for_conflict_check.iter() {
let ((resource_id, required_by, _name), dependency_info) = (entry.key(), entry.value());
let ResolvedDependencyInfo {
version_constraint,
resolved_sha,
parent_version,
parent_sha,
resolution_mode,
} = dependency_info;
if matches!(resolution_mode, crate::resolver::types::ResolutionMode::Version) {
tracing::debug!(
"Adding VERSION path to conflict detector: resource_id={}, required_by={}, version={}, sha={}",
resource_id,
required_by,
version_constraint,
&resolved_sha[..8.min(resolved_sha.len())]
);
self.conflict_detector.add_requirement_with_parent(
resource_id.clone(),
required_by,
version_constraint,
resolved_sha,
parent_version.clone(),
parent_sha.clone(),
);
} else {
tracing::debug!(
"Skipping GIT path for backtracking: resource_id={}, required_by={}, git_ref={}",
resource_id,
required_by,
version_constraint
);
}
}
let conflict_start = std::time::Instant::now();
for entry in self.resolved_deps_for_conflict_check.iter() {
let ((resource_id, required_by, _name), dependency_info) = (entry.key(), entry.value());
let ResolvedDependencyInfo {
version_constraint,
resolved_sha,
parent_version: _parent_version,
parent_sha: _parent_sha,
resolution_mode,
} = dependency_info;
if matches!(resolution_mode, crate::resolver::types::ResolutionMode::GitRef) {
let source_str = resource_id.source();
let source = source_str.unwrap_or("local");
let path = resource_id.name();
tracing::debug!(
"Adding GIT path to SHA conflict detector: source={}, path={}, git_ref={}, sha={}",
source,
path,
version_constraint,
&resolved_sha[..8.min(resolved_sha.len())]
);
self.sha_conflict_detector.add_requirement(
crate::resolver::sha_conflict_detector::ResolvedRequirement {
source: source.to_string(),
path: path.to_string(),
resolved_sha: resolved_sha.clone(),
requested_version: version_constraint.clone(),
required_by: required_by.clone(),
resolution_mode: *resolution_mode,
},
);
}
}
let sha_conflicts = self.sha_conflict_detector.detect_conflicts()?;
let conflict_detect_duration = conflict_start.elapsed();
tracing::debug!(
"Phase 5: SHA conflict detection took {:?} for {} tracked dependencies",
conflict_detect_duration,
self.resolved_deps_for_conflict_check.len()
);
if !sha_conflicts.is_empty() {
let error_messages: Vec<String> =
sha_conflicts.iter().map(|conflict| conflict.format_error()).collect();
return Err(anyhow::anyhow!(
"Unresolvable SHA conflicts detected:\n{}",
error_messages.join("\n")
));
}
let conflicts = self.conflict_detector.detect_conflicts();
if !conflicts.is_empty() {
tracing::info!(
"Detected {} version constraint conflict(s), attempting automatic resolution...",
conflicts.len()
);
let mut backtracker =
backtracking::BacktrackingResolver::new(&self.core, &mut self.version_service);
backtracker.populate_from_conflict_detector(&self.conflict_detector);
match backtracker.resolve_conflicts(&conflicts).await {
Ok(result) if result.resolved => {
if result.total_transitive_reresolutions > 0 {
tracing::info!(
"✓ Resolved conflicts after {} iteration(s): {} version(s) adjusted, {} transitive re-resolution(s)",
result.iterations,
result.updates.len(),
result.total_transitive_reresolutions
);
} else {
tracing::info!(
"✓ Resolved conflicts after {} iteration(s): {} version(s) adjusted",
result.iterations,
result.updates.len()
);
}
for update in &result.updates {
tracing::info!(
" {} : {} → {}",
update.resource_id,
update.old_version,
update.new_version
);
}
self.apply_backtracking_updates(&result.updates).await?;
self.update_lockfile_entries(lockfile, &result.updates)?;
tracing::info!("Applied backtracking updates, backtracking complete");
}
Ok(result) => {
let reason_msg = match result.termination_reason {
backtracking::TerminationReason::MaxIterations => {
format!("reached max iterations ({})", result.iterations)
}
backtracking::TerminationReason::Timeout => "timeout exceeded".to_string(),
backtracking::TerminationReason::NoProgress => {
"no progress made (same conflicts persist)".to_string()
}
backtracking::TerminationReason::Oscillation => {
"oscillation detected (cycling between conflict states)".to_string()
}
backtracking::TerminationReason::NoCompatibleVersion => {
"no compatible version found".to_string()
}
_ => "unknown reason".to_string(),
};
tracing::warn!("Backtracking failed: {}", reason_msg);
let mut error_msg = format!(
"Version conflicts detected (automatic resolution failed: {}):\n\n",
reason_msg
);
for conflict in &conflicts {
error_msg.push_str(&format!("{conflict}\n"));
}
error_msg.push_str(
"\nSuggestion: Manually specify compatible versions in agpm.toml",
);
return Err(anyhow::anyhow!("{}", error_msg));
}
Err(e) => {
tracing::error!("Backtracking error: {}", e);
let mut error_msg = format!(
"Version conflicts detected (automatic resolution error: {}):\n\n",
e
);
for conflict in &conflicts {
error_msg.push_str(&format!("{conflict}\n"));
}
return Err(anyhow::anyhow!("{}", error_msg));
}
}
}
Ok(())
}
fn finalize_resolution(
&mut self,
lockfile: &mut LockFile,
progress: &Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
) -> Result<()> {
self.add_version_to_dependencies(lockfile)?;
self.detect_target_conflicts(lockfile)?;
if let Some(pm) = progress {
let total_resources = lockfile.agents.len()
+ lockfile.commands.len()
+ lockfile.scripts.len()
+ lockfile.hooks.len()
+ lockfile.snippets.len()
+ lockfile.mcp_servers.len()
+ lockfile.skills.len();
pm.complete_phase_with_window(Some(&format!(
"Resolved {} dependencies",
total_resources
)));
}
Ok(())
}
pub async fn pre_sync_sources(
&mut self,
deps: &[(String, ResourceDependency)],
progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
) -> Result<()> {
self.version_service.pre_sync_sources(&self.core, deps, progress).await?;
self.sources_pre_synced.store(true, std::sync::atomic::Ordering::Release);
Ok(())
}
pub async fn update(
&mut self,
existing: &LockFile,
deps_to_update: Option<Vec<String>>,
progress: Option<Arc<crate::utils::MultiPhaseProgress>>,
) -> Result<LockFile> {
match deps_to_update {
None => {
tracing::debug!("Performing full resolution for all dependencies");
self.resolve_with_options(true, progress).await
}
Some(names) => {
tracing::debug!("Incremental update requested for: {:?}", names);
let (unchanged, to_resolve) = Self::filter_lockfile_entries(existing, &names);
if to_resolve.is_empty() {
tracing::warn!("No matching dependencies found in lockfile: {:?}", names);
return Ok(existing.clone());
}
tracing::debug!(
"Resolving {} dependencies, keeping {} unchanged",
to_resolve.len(),
unchanged.agents.len()
+ unchanged.snippets.len()
+ unchanged.commands.len()
+ unchanged.scripts.len()
+ unchanged.hooks.len()
+ unchanged.mcp_servers.len()
+ unchanged.skills.len()
);
let filtered_manifest = self.create_filtered_manifest(&to_resolve);
let mut temp_resolver = DependencyResolver::new_with_context(
filtered_manifest,
self.core.cache().clone(),
self.core.operation_context().cloned(),
)
.await?;
let updated = temp_resolver.resolve_with_options(true, progress).await?;
let merged = Self::merge_lockfiles(unchanged, updated);
tracing::debug!(
"Incremental update complete: merged lockfile has {} total entries",
merged.agents.len()
+ merged.snippets.len()
+ merged.commands.len()
+ merged.scripts.len()
+ merged.hooks.len()
+ merged.mcp_servers.len()
+ merged.skills.len()
);
Ok(merged)
}
}
}
pub async fn get_available_versions(&self, repo_path: &Path) -> Result<Vec<String>> {
VersionResolutionService::get_available_versions(&self.core, repo_path).await
}
pub async fn verify(&self, _lockfile: &LockFile) -> Result<()> {
Ok(())
}
pub fn operation_context(&self) -> Option<&Arc<OperationContext>> {
self.core.operation_context()
}
pub fn set_operation_context(&mut self, context: Arc<OperationContext>) {
self.core.operation_context = Some(context);
}
}
impl DependencyResolver {
fn build_manifest_override_index(
&self,
base_deps: &[(String, ResourceDependency, ResourceType)],
) -> types::ManifestOverrideIndex {
use crate::resolver::types::{ManifestOverride, OverrideKey, normalize_lookup_path};
let mut index = HashMap::new();
for (name, dep, resource_type) in base_deps {
if dep.is_pattern() {
continue;
}
let normalized_path = normalize_lookup_path(dep.get_path());
let source = dep.get_source().map(std::string::ToString::to_string);
let tool = dep
.get_tool()
.map(str::to_string)
.unwrap_or_else(|| self.core.manifest().get_default_tool(*resource_type));
let merged_variant_inputs =
lockfile_builder::build_merged_variant_inputs(self.core.manifest(), dep);
let variant_hash = crate::utils::compute_variant_inputs_hash(&merged_variant_inputs)
.unwrap_or_else(|_| crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string());
let key = OverrideKey {
resource_type: *resource_type,
normalized_path,
source,
tool,
variant_hash,
};
let override_info = ManifestOverride {
filename: dep.get_filename().map(std::string::ToString::to_string),
target: dep.get_target().map(std::string::ToString::to_string),
install: dep.get_install(),
manifest_alias: Some(name.clone()),
template_vars: dep.get_template_vars().cloned(),
};
tracing::debug!(
"Adding manifest override for {:?}:{} (tool={}, variant_hash={})",
resource_type,
dep.get_path(),
key.tool,
key.variant_hash
);
index.insert(key, override_info);
}
tracing::info!("Built manifest override index with {} entries", index.len());
index
}
async fn resolve_transitive_dependencies(
&mut self,
base_deps: &[(String, ResourceDependency, ResourceType)],
progress: Option<std::sync::Arc<crate::utils::MultiPhaseProgress>>,
) -> Result<Vec<(String, ResourceDependency, ResourceType)>> {
use crate::resolver::transitive_resolver;
let manifest_overrides = self.build_manifest_override_index(base_deps);
let resolution_ctx = ResolutionContext {
manifest: self.core.manifest(),
cache: self.core.cache(),
source_manager: self.core.source_manager(),
operation_context: self.core.operation_context(),
};
let mut ctx = TransitiveContext {
base: resolution_ctx,
dependency_map: &self.dependency_map,
transitive_custom_names: &self.transitive_custom_names,
conflict_detector: &mut self.conflict_detector,
manifest_overrides: &manifest_overrides,
};
let prepared_versions = self.version_service.prepared_versions_ready_arc();
let services = transitive_resolver::ResolutionServices {
version_service: &self.version_service,
pattern_service: &self.pattern_service,
};
transitive_resolver::resolve_with_services(
transitive_resolver::TransitiveResolutionParams {
ctx: &mut ctx,
core: &self.core,
base_deps,
enable_transitive: true,
prepared_versions: &prepared_versions,
pattern_alias_map: &self.pattern_alias_map,
services: &services,
progress,
},
)
.await
}
fn get_dependencies_for(
&self,
name: &str,
source: Option<&str>,
resource_type: ResourceType,
tool: Option<&str>,
variant_hash: &str,
) -> Vec<String> {
let key = (
resource_type,
name.to_string(),
source.map(std::string::ToString::to_string),
tool.map(std::string::ToString::to_string),
variant_hash.to_string(),
);
let result = self.dependency_map.get(&key).map(|v| v.clone()).unwrap_or_default();
tracing::debug!(
"[DEBUG] get_dependencies_for: name='{}', type={:?}, source={:?}, tool={:?}, hash={}, found={} deps",
name,
resource_type,
source,
tool,
&variant_hash[..8],
result.len()
);
result
}
fn get_pattern_alias_for_dependency(
&self,
name: &str,
resource_type: ResourceType,
) -> Option<String> {
self.pattern_alias_map.get(&(resource_type, name.to_string())).map(|v| v.clone())
}
}
#[cfg(test)]
mod resolver_tests {
use super::*;
#[tokio::test]
async fn test_resolver_creation() -> Result<()> {
let manifest = Manifest::default();
let cache = Cache::new()?;
DependencyResolver::new(manifest, cache).await?;
Ok(())
}
#[tokio::test]
async fn test_resolver_with_global() -> Result<()> {
let manifest = Manifest::default();
let cache = Cache::new()?;
DependencyResolver::new_with_global(manifest, cache).await?;
Ok(())
}
}