mod buildah;
pub mod buildah_sidecar;
#[cfg(target_os = "windows")]
pub mod hcs;
#[cfg(target_os = "macos")]
mod sandbox;
pub use buildah::BuildahBackend;
pub use buildah_sidecar::BuildahSidecarBackend;
#[cfg(target_os = "windows")]
pub use hcs::HcsBackend;
#[cfg(target_os = "macos")]
pub use sandbox::SandboxBackend;
use std::path::Path;
use std::sync::Arc;
use crate::builder::{BuildOptions, BuiltImage, RegistryAuth};
use crate::dockerfile::Dockerfile;
use crate::error::{BuildError, Result};
use crate::tui::BuildEvent;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum ImageOs {
#[default]
Linux,
Windows,
}
#[derive(thiserror::Error, Debug)]
#[error("unknown OS: {0} (expected linux or windows)")]
pub struct ImageOsParseError(pub String);
impl std::str::FromStr for ImageOs {
type Err = ImageOsParseError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let os_part = s.split('/').next().unwrap_or("").trim();
match os_part.to_ascii_lowercase().as_str() {
"linux" => Ok(ImageOs::Linux),
"windows" => Ok(ImageOs::Windows),
_ => Err(ImageOsParseError(s.to_string())),
}
}
}
#[async_trait::async_trait]
pub trait BuildBackend: Send + Sync {
async fn build_image(
&self,
context: &Path,
dockerfile: &Dockerfile,
options: &BuildOptions,
event_tx: Option<std::sync::mpsc::Sender<BuildEvent>>,
) -> Result<BuiltImage>;
async fn push_image(&self, tag: &str, auth: Option<&RegistryAuth>) -> Result<()>;
async fn tag_image(&self, image: &str, new_tag: &str) -> Result<()>;
async fn manifest_create(&self, name: &str) -> Result<()>;
async fn manifest_add(&self, manifest: &str, image: &str) -> Result<()>;
async fn manifest_push(&self, name: &str, destination: &str) -> Result<()>;
async fn is_available(&self) -> bool;
fn name(&self) -> &'static str;
}
pub async fn detect_backend(target_os: ImageOs) -> Result<Arc<dyn BuildBackend>> {
detect_backend_with_options(target_os, None).await
}
pub async fn detect_backend_with_options(
target_os: ImageOs,
options: Option<&BuildOptions>,
) -> Result<Arc<dyn BuildBackend>> {
use zlayer_types::builder::BuilderBackendKind;
if let Some(opts) = options {
if let Some(kind) = opts.backend_override {
return construct_backend(kind, target_os).await;
}
}
if let Ok(env_val) = std::env::var("ZLAYER_BACKEND") {
let parsed: BuilderBackendKind =
env_val
.parse()
.map_err(|e: String| BuildError::NotSupported {
operation: format!("ZLAYER_BACKEND={env_val}: {e}"),
})?;
return construct_backend(parsed, target_os).await;
}
#[cfg(target_os = "windows")]
{
match target_os {
ImageOs::Linux => Err(BuildError::NotSupported {
operation: "Linux image building on Windows hosts requires a Linux peer \
(Phase L follow-up will add WSL2-buildah routing)"
.to_string(),
}),
ImageOs::Windows => {
let backend = HcsBackend::new().await?;
Ok(Arc::new(backend))
}
}
}
#[cfg(target_os = "macos")]
{
match target_os {
ImageOs::Linux => {
if let Some(sidecar) = sidecar_from_env() {
if sidecar.is_available().await {
return Ok(Arc::new(sidecar));
}
tracing::warn!(
"ZLAYER_BUILDD_ADDR set but sidecar not reachable; \
falling back to buildah-cli / sandbox"
);
}
if let Ok(backend) = BuildahBackend::try_new().await {
Ok(Arc::new(backend))
} else {
tracing::info!(
"Buildah not available on macOS, falling back to sandbox backend"
);
Ok(Arc::new(SandboxBackend::default()))
}
}
ImageOs::Windows => Err(BuildError::NotSupported {
operation: "building Windows images requires a Windows host — run this build \
on a Windows node of the ZLayer cluster"
.to_string(),
}),
}
}
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
{
match target_os {
ImageOs::Linux => {
let sidecar = BuildahSidecarBackend::default();
if sidecar.is_available().await {
Ok(Arc::new(sidecar))
} else {
tracing::debug!(
"buildah-sidecar unavailable on Linux host, falling back to buildah-cli"
);
let cli = BuildahBackend::new().await?;
Ok(Arc::new(cli))
}
}
ImageOs::Windows => Err(BuildError::NotSupported {
operation: "building Windows images requires a Windows host — run this build \
on a Windows node of the ZLayer cluster"
.to_string(),
}),
}
}
}
#[cfg(target_os = "macos")]
fn sidecar_from_env() -> Option<BuildahSidecarBackend> {
use std::path::PathBuf;
use zlayer_types::builder::SidecarConfig;
let addr = std::env::var("ZLAYER_BUILDD_ADDR").ok()?;
let tls_dir = std::env::var("ZLAYER_BUILDD_TLS_DIR")
.ok()
.map(PathBuf::from);
let context_mount = std::env::var("ZLAYER_BUILDD_CONTEXT_MOUNT")
.ok()
.and_then(|s| {
s.split_once(':')
.map(|(h, g)| (PathBuf::from(h), PathBuf::from(g)))
});
Some(BuildahSidecarBackend::new(SidecarConfig {
addr: Some(addr),
tls_dir,
context_mount,
..Default::default()
}))
}
#[cfg_attr(windows, allow(clippy::needless_return))]
async fn construct_backend(
kind: zlayer_types::builder::BuilderBackendKind,
target_os: ImageOs,
) -> Result<Arc<dyn BuildBackend>> {
use zlayer_types::builder::BuilderBackendKind;
match kind {
BuilderBackendKind::BuildahCli => {
if target_os != ImageOs::Linux {
return Err(BuildError::NotSupported {
operation: format!(
"buildah-cli backend can only build Linux images, requested target_os={target_os:?}"
),
});
}
#[cfg(target_os = "windows")]
{
return Err(BuildError::NotSupported {
operation: "buildah-cli backend is not available on Windows hosts \
(requires a Linux peer)"
.to_string(),
});
}
#[cfg(not(target_os = "windows"))]
{
let backend = BuildahBackend::new().await?;
Ok(Arc::new(backend))
}
}
BuilderBackendKind::BuildahSidecar => {
if target_os != ImageOs::Linux {
return Err(BuildError::NotSupported {
operation: format!(
"buildah-sidecar backend can only build Linux images, requested target_os={target_os:?}"
),
});
}
#[cfg(target_os = "linux")]
{
Ok(Arc::new(BuildahSidecarBackend::default()))
}
#[cfg(target_os = "macos")]
{
Ok(Arc::new(sidecar_from_env().unwrap_or_default()))
}
#[cfg(all(not(target_os = "linux"), not(target_os = "macos")))]
{
Err(BuildError::NotSupported {
operation: "buildah-sidecar backend is not available on this host \
(zlayer-buildd runs on Linux or in a macOS VZ container)"
.to_string(),
})
}
}
BuilderBackendKind::Sandbox => {
if target_os != ImageOs::Linux {
return Err(BuildError::NotSupported {
operation: format!(
"macOS sandbox backend can only build Linux images, requested target_os={target_os:?}"
),
});
}
#[cfg(target_os = "macos")]
{
Ok(Arc::new(SandboxBackend::default()))
}
#[cfg(not(target_os = "macos"))]
{
Err(BuildError::NotSupported {
operation: "sandbox backend is only available on macOS hosts".to_string(),
})
}
}
BuilderBackendKind::Hcs => {
if target_os != ImageOs::Windows {
return Err(BuildError::NotSupported {
operation: format!(
"HCS backend can only build Windows images, requested target_os={target_os:?}"
),
});
}
#[cfg(target_os = "windows")]
{
let backend = HcsBackend::new().await?;
Ok(Arc::new(backend))
}
#[cfg(not(target_os = "windows"))]
{
Err(BuildError::NotSupported {
operation: "HCS backend is only available on Windows hosts".to_string(),
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use zlayer_types::builder::BuilderBackendKind;
#[test]
fn image_os_parses_simple_and_slash_form() {
assert_eq!("linux".parse::<ImageOs>().unwrap(), ImageOs::Linux);
assert_eq!("Linux".parse::<ImageOs>().unwrap(), ImageOs::Linux);
assert_eq!("windows".parse::<ImageOs>().unwrap(), ImageOs::Windows);
assert_eq!("linux/amd64".parse::<ImageOs>().unwrap(), ImageOs::Linux);
assert_eq!(
"windows/amd64".parse::<ImageOs>().unwrap(),
ImageOs::Windows
);
assert!("darwin".parse::<ImageOs>().is_err());
}
#[tokio::test]
async fn construct_backend_rejects_mismatched_target_os() {
fn assert_not_supported(result: Result<Arc<dyn BuildBackend>>, label: &str) {
match result {
Ok(_) => panic!("{label}: expected NotSupported, got Ok"),
Err(BuildError::NotSupported { .. }) => {}
Err(other) => panic!("{label}: expected NotSupported, got: {other:?}"),
}
}
assert_not_supported(
construct_backend(BuilderBackendKind::Hcs, ImageOs::Linux).await,
"HCS + Linux target",
);
assert_not_supported(
construct_backend(BuilderBackendKind::BuildahCli, ImageOs::Windows).await,
"BuildahCli + Windows target",
);
assert_not_supported(
construct_backend(BuilderBackendKind::BuildahSidecar, ImageOs::Windows).await,
"BuildahSidecar + Windows target",
);
assert_not_supported(
construct_backend(BuilderBackendKind::Sandbox, ImageOs::Windows).await,
"Sandbox + Windows target",
);
}
#[cfg(target_os = "linux")]
#[tokio::test]
#[ignore = "mutates PATH / ZLAYER_BUILDD_BIN / ZLAYER_DATA_DIR; serialize manually"]
#[allow(unsafe_code, clippy::await_holding_lock)]
async fn detect_backend_falls_back_to_cli_when_sidecar_missing() {
let _g = crate::TEST_ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let prev_path = std::env::var_os("PATH");
let prev_buildd = std::env::var_os("ZLAYER_BUILDD_BIN");
let prev_data = std::env::var_os("ZLAYER_DATA_DIR");
let prev_backend = std::env::var_os("ZLAYER_BACKEND");
let tmp = tempfile::tempdir().expect("tempdir");
unsafe {
std::env::remove_var("ZLAYER_BUILDD_BIN");
std::env::remove_var("ZLAYER_BACKEND");
std::env::set_var("PATH", tmp.path());
std::env::set_var("ZLAYER_DATA_DIR", tmp.path());
}
let result = detect_backend(ImageOs::Linux).await;
unsafe {
match prev_path {
Some(v) => std::env::set_var("PATH", v),
None => std::env::remove_var("PATH"),
}
match prev_buildd {
Some(v) => std::env::set_var("ZLAYER_BUILDD_BIN", v),
None => std::env::remove_var("ZLAYER_BUILDD_BIN"),
}
match prev_data {
Some(v) => std::env::set_var("ZLAYER_DATA_DIR", v),
None => std::env::remove_var("ZLAYER_DATA_DIR"),
}
match prev_backend {
Some(v) => std::env::set_var("ZLAYER_BACKEND", v),
None => std::env::remove_var("ZLAYER_BACKEND"),
}
}
match result {
Ok(backend) => assert_eq!(
backend.name(),
"buildah",
"expected CLI fallback ('buildah'), got: {}",
backend.name(),
),
Err(e) => {
eprintln!("detect_backend returned err (no buildah on PATH?): {e}");
}
}
}
}