use anyhow::{bail, Context, Result};
use std::path::Path;
use std::process::Command;
use tracing::{debug, info, warn};
use super::dockerfile_ssl::{
preprocess_dockerfile_for_ssl, SslCertContext, SSL_CERT_BUILD_CONTEXT,
};
use super::registry::docker_push;
pub(crate) fn configure_buildx_output(
cmd: &mut Command,
push: bool,
buildx_supports_push: bool,
) -> bool {
if push && buildx_supports_push {
cmd.arg("--push");
false
} else {
cmd.arg("--load");
push
}
}
pub(crate) fn is_buildx_available(container_cli: &str) -> bool {
Command::new(container_cli)
.args(["buildx", "version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub(crate) struct DockerBuildOptions<'a> {
pub app_path: &'a str,
pub dockerfile: Option<&'a str>,
pub image_tag: &'a str,
pub container_cli: &'a str,
pub buildx_supports_push: bool,
pub use_buildx: bool,
pub push: bool,
pub buildkit_host: Option<&'a str>,
pub env: &'a [String],
pub build_context: Option<&'a str>,
pub build_contexts: &'a std::collections::HashMap<String, String>,
pub no_cache: bool,
}
pub(crate) fn build_image_with_dockerfile(options: DockerBuildOptions) -> Result<()> {
let cli_check = Command::new(options.container_cli)
.arg("--version")
.output();
if cli_check.is_err() {
bail!(
"{} CLI not found. Please install Docker or Podman.",
options.container_cli
);
}
let ssl_cert_path = super::resolve_ssl_cert_file();
if ssl_cert_path.is_some() && !options.use_buildx {
warn!(
"SSL_CERT_FILE is set but docker:build does not support BuildKit features \
required for SSL certificate bind mounts. Use 'docker:buildx' backend for \
SSL certificate support during builds."
);
}
let (_temp_dir, effective_dockerfile) = if options.use_buildx && ssl_cert_path.is_some() {
let original_dockerfile = options
.dockerfile
.map(|df| Path::new(options.app_path).join(df))
.unwrap_or_else(|| Path::new(options.app_path).join("Dockerfile"));
if original_dockerfile.exists() {
info!("SSL_CERT_FILE detected, preprocessing Dockerfile for bind mounts");
let (temp_dir, processed_path) = preprocess_dockerfile_for_ssl(&original_dockerfile)?;
(Some(temp_dir), Some(processed_path))
} else {
(
None,
options
.dockerfile
.map(|df| Path::new(options.app_path).join(df)),
)
}
} else {
(
None,
options
.dockerfile
.map(|df| Path::new(options.app_path).join(df)),
)
};
let mut cmd = Command::new(options.container_cli);
if options.use_buildx {
if !is_buildx_available(options.container_cli) {
bail!(
"{} buildx not available. Install it or use docker:build backend instead.",
options.container_cli
);
}
cmd.arg("buildx");
info!(
"Building image with {} buildx: {}",
options.container_cli, options.image_tag
);
} else {
info!(
"Building image with {}: {}",
options.container_cli, options.image_tag
);
}
cmd.arg("build").arg("-t").arg(options.image_tag);
if options.no_cache {
cmd.arg("--no-cache");
}
if let Some(ref df) = effective_dockerfile {
cmd.arg("-f").arg(df);
}
cmd.arg("--platform").arg("linux/amd64");
let _ssl_cert_context: Option<SslCertContext> = if options.use_buildx {
if let Some(ref cert_path) = ssl_cert_path {
let context = SslCertContext::new(cert_path)?;
cmd.arg("--build-context").arg(format!(
"{}={}",
SSL_CERT_BUILD_CONTEXT,
context.context_path.display()
));
Some(context)
} else {
None
}
} else {
None
};
let proxy_vars = super::proxy::read_and_transform_proxy_vars();
if !proxy_vars.is_empty() {
let container_name = options
.buildkit_host
.map(|h| h.strip_prefix("docker-container://").unwrap_or(h));
let effective_proxy_vars = super::buildkit::resolve_and_apply_host_gateway(
&mut cmd,
options.container_cli,
&proxy_vars,
container_name,
options.buildkit_host.is_some(),
);
info!("Injecting proxy variables for docker build");
for (key, value) in &effective_proxy_vars {
cmd.arg("--build-arg").arg(format!("{}={}", key, value));
}
}
for build_arg in options.env {
cmd.arg("--build-arg").arg(build_arg);
}
if !options.build_contexts.is_empty() {
info!("Using {} build context(s)", options.build_contexts.len());
for (name, path) in options.build_contexts {
cmd.arg("--build-context").arg(format!("{}={}", name, path));
debug!("Build context: {}={}", name, path);
}
}
let context_path = options.build_context.unwrap_or(options.app_path);
cmd.arg(context_path);
if options.use_buildx {
if let Some(host) = options.buildkit_host {
cmd.env("BUILDKIT_HOST", host);
}
}
let needs_fallback_push = if options.use_buildx {
configure_buildx_output(&mut cmd, options.push, options.buildx_supports_push)
} else {
options.push
};
debug!("Executing command: {:?}", cmd);
let status = cmd
.status()
.with_context(|| format!("Failed to execute {} build", options.container_cli))?;
if !status.success() {
bail!(
"{} build failed with status: {}",
options.container_cli,
status
);
}
if needs_fallback_push {
docker_push(options.container_cli, options.image_tag)?;
}
Ok(())
}