use anyhow::{Context, Result, bail};
use std::path::Path;
use std::process::Command;
use crate::config::BuildConfig;
use crate::docker_backend::{ContainerRuntime, docker_available, podman_available};
use crate::languages::dockerfile_image_name;
#[derive(Debug)]
pub struct BuildResult {
pub image: String,
#[allow(dead_code)]
pub cached: bool,
}
pub fn image_exists(image: &str, runtime: ContainerRuntime) -> bool {
let cmd = runtime.cmd();
Command::new(cmd)
.args(["image", "inspect", image])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn build_image(
project_name: &str,
dockerfile_path: &Path,
context_path: &Path,
config: &BuildConfig,
) -> Result<BuildResult> {
let runtime = if docker_available() {
ContainerRuntime::Docker
} else if podman_available() {
ContainerRuntime::Podman
} else {
bail!("No container runtime available (need Docker or Podman)");
};
let image_name = dockerfile_image_name(project_name, dockerfile_path);
if !config.no_cache && image_exists(&image_name, runtime) {
eprintln!("Using cached image: {}", image_name);
return Ok(BuildResult {
image: image_name,
cached: true,
});
}
eprintln!("Building image from {}...", dockerfile_path.display());
let mut args = vec![
"build".to_string(),
"-t".to_string(),
image_name.clone(),
"-f".to_string(),
dockerfile_path.to_string_lossy().to_string(),
];
if let Some(ref target) = config.target {
args.push("--target".to_string());
args.push(target.clone());
}
for (key, value) in &config.args {
args.push("--build-arg".to_string());
args.push(format!("{}={}", key, value));
}
if config.no_cache {
args.push("--no-cache".to_string());
}
args.push(context_path.to_string_lossy().to_string());
let output = Command::new(runtime.cmd())
.args(&args)
.output()
.context("Failed to run docker build")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Docker build failed:\n{}", stderr);
}
eprintln!("Built image: {}", image_name);
Ok(BuildResult {
image: image_name,
cached: false,
})
}
pub fn build_or_use_image(
project_name: &str,
base_image: &str,
base_dir: &Path,
config: &crate::config::Config,
) -> Result<String> {
if let Some(dockerfile_path) = config.dockerfile_path(base_dir) {
let context_path = config.build_context(base_dir, &dockerfile_path);
let result = build_image(project_name, &dockerfile_path, &context_path, &config.build)?;
Ok(result.image)
} else {
Ok(base_image.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_image_name_generation() {
let dir = tempdir().unwrap();
let dockerfile_path = dir.path().join("Dockerfile");
let mut file = std::fs::File::create(&dockerfile_path).unwrap();
writeln!(file, "FROM alpine:3.20").unwrap();
let name = dockerfile_image_name("my-project", &dockerfile_path);
assert!(name.starts_with("agentkernel-my-project:"));
}
}