use crate::cli::args::RunArgs;
use crate::config::Config;
use crate::error::MinoResult;
use crate::layer::{
build_layer_manifest, compose_image, compute_path_prepend, merge_layer_env,
needs_compose_build, resolve_layers, ResolvedLayer,
};
use crate::orchestration::ContainerRuntime;
use crate::ui::{BuildProgress, TaskSpinner, UiContext};
use std::collections::HashMap;
use std::path::Path;
use tracing::debug;
use super::ImageResolution;
const IMAGE_REGISTRY: &str = "ghcr.io/dean0x";
pub(crate) const LAYER_BASE_IMAGE: &str = "ghcr.io/dean0x/mino-base:latest";
pub(super) fn parse_layers_env(val: &str) -> Vec<String> {
val.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
pub(super) fn resolve_layer_names(args: &RunArgs, config: &Config) -> Option<Vec<String>> {
if !args.layers.is_empty() {
return Some(args.layers.clone());
}
if args.image.is_some() {
return None;
}
if let Ok(val) = std::env::var("MINO_LAYERS") {
let layers = parse_layers_env(&val);
if !layers.is_empty() {
return Some(layers);
}
}
if !config.container.layers.is_empty() {
return Some(config.container.layers.clone());
}
None
}
pub(super) fn image_alias_to_layer(image: &str) -> Option<&str> {
match image {
"typescript" | "ts" | "node" => Some("typescript"),
"rust" | "cargo" => Some("rust"),
"python" | "py" => Some("python"),
_ => None,
}
}
pub(super) fn resolve_image_alias(image: &str) -> String {
if image.contains('/') || image.contains(':') {
return image.to_string();
}
match image {
"base" => format!("{}/mino-base:latest", IMAGE_REGISTRY),
other => other.to_string(),
}
}
pub(super) fn is_default_image(args: &RunArgs, config: &Config) -> bool {
args.image.is_none() && config.container.image == "fedora:43"
}
pub(super) fn resolve_final_image(raw_image: &str, base_only: bool) -> ImageResolution {
let image = if base_only {
debug!("Using base image without layers: {}", LAYER_BASE_IMAGE);
LAYER_BASE_IMAGE.to_string()
} else {
resolve_image_alias(raw_image)
};
ImageResolution {
image,
layer_env: HashMap::new(),
}
}
fn inject_bootstrap_env(
layer_env: &mut HashMap<String, String>,
resolved: &[ResolvedLayer],
) -> MinoResult<()> {
if let Some(manifest_json) = build_layer_manifest(resolved)? {
layer_env.insert("MINO_LAYER_MANIFEST".to_string(), manifest_json);
}
if let Some(path_prepend) = compute_path_prepend(resolved) {
layer_env
.entry("MINO_PATH_PREPEND".to_string())
.or_insert(path_prepend);
}
Ok(())
}
pub(super) async fn resolve_image(
args: &RunArgs,
config: &Config,
ctx: &UiContext,
spinner: &mut TaskSpinner,
runtime: &dyn ContainerRuntime,
project_dir: &Path,
) -> MinoResult<(ImageResolution, bool)> {
let raw_image = args
.image
.clone()
.unwrap_or_else(|| config.container.image.clone());
let layer_names = resolve_layer_names(args, config)
.or_else(|| image_alias_to_layer(&raw_image).map(|name| vec![name.to_string()]));
let (layer_names, base_only) =
if layer_names.is_none() && ctx.is_interactive() && is_default_image(args, config) {
spinner.clear();
match super::prompts::prompt_layer_selection(ctx, project_dir).await? {
Some(selected) => {
spinner.start("Initializing sandbox...");
(Some(selected), false)
}
None => (None, true),
}
} else {
(layer_names, false)
};
let using_layers = layer_names.is_some() || base_only;
let resolution = if let Some(names) = layer_names {
let mut resolved = Vec::new();
for name in &names {
spinner.message(&format!("Resolving layer: {}...", name));
let mut layers = resolve_layers(std::slice::from_ref(name), project_dir).await?;
resolved.append(&mut layers);
}
if needs_compose_build(&resolved) {
spinner.clear();
let label = names.join(", ");
let progress = BuildProgress::new(ctx, &label);
let result = compose_image(
runtime,
LAYER_BASE_IMAGE,
&resolved,
Some(&|line: String| progress.on_line(line)),
)
.await;
progress.finish();
let result = result?;
let action = if result.was_cached { "cached" } else { "built" };
debug!("Using {} composed image: {}", action, result.image_tag);
let mut layer_env = result.env;
inject_bootstrap_env(&mut layer_env, &resolved)?;
ImageResolution {
image: result.image_tag,
layer_env,
}
} else {
spinner.message("Layers will install on first run via bootstrap...");
debug!("All layers are user-install only, skipping compose");
let mut layer_env = merge_layer_env(&resolved, false);
inject_bootstrap_env(&mut layer_env, &resolved)?;
ImageResolution {
image: LAYER_BASE_IMAGE.to_string(),
layer_env,
}
}
} else {
resolve_final_image(&raw_image, base_only)
};
Ok((resolution, using_layers))
}