use crate::cache::{
detect_lockfiles, format_bytes, gb_to_bytes, resolve_state, CacheMount, CacheSidecar,
CacheSizeStatus, CacheState, CacheVolume, LockfileInfo,
};
use crate::cli::args::RunArgs;
use crate::config::Config;
use crate::error::{MinoError, MinoResult};
use crate::orchestration::ContainerRuntime;
use console::style;
use std::collections::HashMap;
use std::path::Path;
use tracing::{debug, warn};
use super::CacheSession;
pub(super) async fn setup_caches(
runtime: &dyn ContainerRuntime,
args: &RunArgs,
config: &Config,
project_dir: &Path,
) -> MinoResult<(Vec<CacheMount>, HashMap<String, String>, CacheSession)> {
let mut cache_session = CacheSession::default();
let mut cache_mounts = Vec::new();
let mut cache_env = HashMap::new();
if args.no_cache || !config.cache.enabled {
debug!("Caching disabled");
return Ok((cache_mounts, cache_env, cache_session));
}
let lockfiles = {
let dir = project_dir.to_path_buf();
tokio::task::spawn_blocking(move || detect_lockfiles(&dir))
.await
.map_err(|e| MinoError::Internal(format!("lockfile detection task failed: {e}")))?
}?;
if lockfiles.is_empty() {
debug!("No lockfiles detected, skipping cache setup");
return Ok((cache_mounts, cache_env, cache_session));
}
debug!("Detected {} lockfile(s)", lockfiles.len());
for info in &lockfiles {
let (mount, should_finalize) =
setup_cache_for_lockfile(runtime, info, args.cache_fresh).await?;
for (key, value) in info.ecosystem.cache_env_vars() {
cache_env.insert(key.to_string(), value.to_string());
}
if should_finalize {
cache_session
.volumes_to_finalize
.push(mount.volume_name.clone());
}
cache_mounts.push(mount);
}
cache_env.insert("XDG_CACHE_HOME".to_string(), "/cache/xdg".to_string());
Ok((cache_mounts, cache_env, cache_session))
}
async fn setup_cache_for_lockfile(
runtime: &dyn ContainerRuntime,
info: &LockfileInfo,
force_fresh: bool,
) -> MinoResult<(CacheMount, bool)> {
let volume_name = info.volume_name();
if force_fresh {
CacheSidecar::delete(&volume_name).await.ok();
}
let existing = if force_fresh {
None
} else {
runtime.volume_inspect(&volume_name).await?
};
let should_finalize = match existing {
Some(vol_info) => {
let label_state = CacheVolume::from_labels(&vol_info.name, &vol_info.labels)
.map(|c| c.state)
.unwrap_or(CacheState::Building);
let resolved = resolve_state(&volume_name, label_state).await;
match resolved {
CacheState::Complete => {
debug!(
"Cache hit for {} ({}), reusing complete cache",
info.ecosystem,
&info.hash[..8]
);
false
}
CacheState::Building | CacheState::Miss => {
debug!(
"Resuming incomplete cache for {} ({})",
info.ecosystem,
&info.hash[..8]
);
if CacheSidecar::load(&volume_name)
.await
.ok()
.flatten()
.is_none()
{
let mut sidecar = CacheSidecar::new(
volume_name.clone(),
info.ecosystem,
info.hash.clone(),
CacheState::Building,
);
if let Err(e) = sidecar.save().await {
warn!("Failed to backfill sidecar for {}: {}", volume_name, e);
}
}
true
}
}
}
None => {
debug!(
"Cache miss for {} ({}), creating volume",
info.ecosystem,
&info.hash[..8]
);
let cache = CacheVolume::from_lockfile(info, CacheState::Building);
runtime.volume_create(&volume_name, &cache.labels()).await?;
let mut sidecar = CacheSidecar::new(
volume_name.clone(),
info.ecosystem,
info.hash.clone(),
CacheState::Building,
);
if let Err(e) = sidecar.save().await {
warn!("Failed to create sidecar for {}: {}", volume_name, e);
}
let resolved = match runtime.volume_inspect(&volume_name).await? {
Some(vol_info) => {
let label_state = CacheVolume::from_labels(&vol_info.name, &vol_info.labels)
.map(|c| c.state)
.unwrap_or(CacheState::Building);
resolve_state(&volume_name, label_state).await
}
None => CacheState::Building,
};
resolved != CacheState::Complete
}
};
let mount = CacheMount {
volume_name,
container_path: "/cache".to_string(),
ecosystem: info.ecosystem,
};
Ok((mount, should_finalize))
}
pub(super) async fn finalize_caches(cache_session: &CacheSession) {
for volume_name in &cache_session.volumes_to_finalize {
debug!("Finalizing cache: {}", volume_name);
match CacheSidecar::load(volume_name).await {
Ok(Some(mut sidecar)) => {
if let Err(e) = sidecar.mark_complete().await {
warn!("Failed to finalize cache sidecar {}: {}", volume_name, e);
} else {
debug!("Cache {} finalized (complete via sidecar)", volume_name);
}
}
Ok(None) => {
warn!(
"No sidecar found for cache {}, skipping finalization",
volume_name
);
}
Err(e) => {
warn!("Failed to load cache sidecar {}: {}", volume_name, e);
}
}
}
}
pub(super) async fn check_cache_size_warning(runtime: &dyn ContainerRuntime, config: &Config) {
let sizes = match runtime.volume_disk_usage("mino-cache-").await {
Ok(s) => s,
Err(_) => return, };
let total_size: u64 = sizes.values().sum();
let limit_bytes = gb_to_bytes(config.cache.max_total_gb);
if limit_bytes == 0 {
return;
}
let status = CacheSizeStatus::from_usage(total_size, limit_bytes);
let percent = CacheSizeStatus::percentage(total_size, limit_bytes);
match status {
CacheSizeStatus::Ok => {}
CacheSizeStatus::Warning => {
eprintln!(
"{} Cache usage at {:.0}% ({} / {}). Consider running: mino cache gc",
style("!").yellow(),
percent,
format_bytes(total_size),
format_bytes(limit_bytes)
);
}
CacheSizeStatus::Exceeded => {
eprintln!(
"{} Cache limit exceeded! {:.0}% ({} / {}). Run: mino cache gc",
style("!").red().bold(),
percent,
format_bytes(total_size),
format_bytes(limit_bytes)
);
}
}
}