Skip to main content

zlayer_builder/backend/
mod.rs

1//! Build backend abstraction.
2//!
3//! Provides a unified [`BuildBackend`] trait that decouples the build orchestration
4//! logic from the underlying container tooling. Platform-specific implementations
5//! are selected at runtime via [`detect_backend`].
6//!
7//! # Backends
8//!
9//! - [`BuildahBackend`] — wraps the `buildah` CLI (Linux + macOS with buildah installed).
10//! - [`SandboxBackend`] — macOS-only, uses the Seatbelt sandbox when buildah is unavailable.
11
12mod buildah;
13#[cfg(target_os = "macos")]
14mod sandbox;
15
16pub use buildah::BuildahBackend;
17#[cfg(target_os = "macos")]
18pub use sandbox::SandboxBackend;
19
20use std::path::Path;
21use std::sync::Arc;
22
23use crate::builder::{BuildOptions, BuiltImage, RegistryAuth};
24use crate::dockerfile::Dockerfile;
25use crate::error::{BuildError, Result};
26use crate::tui::BuildEvent;
27
28/// A pluggable build backend.
29///
30/// Implementations handle the low-level mechanics of building, pushing, tagging,
31/// and managing manifest lists for container images.
32#[async_trait::async_trait]
33pub trait BuildBackend: Send + Sync {
34    /// Build a container image from a parsed Dockerfile.
35    ///
36    /// # Arguments
37    ///
38    /// * `context`    — path to the build context directory
39    /// * `dockerfile` — parsed Dockerfile IR
40    /// * `options`    — build configuration (tags, args, caching, etc.)
41    /// * `event_tx`   — optional channel for streaming progress events to a TUI
42    async fn build_image(
43        &self,
44        context: &Path,
45        dockerfile: &Dockerfile,
46        options: &BuildOptions,
47        event_tx: Option<std::sync::mpsc::Sender<BuildEvent>>,
48    ) -> Result<BuiltImage>;
49
50    /// Push an image to a container registry.
51    async fn push_image(&self, tag: &str, auth: Option<&RegistryAuth>) -> Result<()>;
52
53    /// Tag an existing image with a new name.
54    async fn tag_image(&self, image: &str, new_tag: &str) -> Result<()>;
55
56    /// Create a new (empty) manifest list.
57    async fn manifest_create(&self, name: &str) -> Result<()>;
58
59    /// Add an image to an existing manifest list.
60    async fn manifest_add(&self, manifest: &str, image: &str) -> Result<()>;
61
62    /// Push a manifest list (and all referenced images) to a registry.
63    async fn manifest_push(&self, name: &str, destination: &str) -> Result<()>;
64
65    /// Returns `true` if the backend tooling is installed and functional.
66    async fn is_available(&self) -> bool;
67
68    /// Human-readable name for this backend (e.g. `"buildah"`, `"sandbox"`).
69    fn name(&self) -> &'static str;
70}
71
72/// Auto-detect the best available build backend for the current platform.
73///
74/// Selection order:
75///
76/// 1. If the `ZLAYER_BACKEND` environment variable is set to `"buildah"` or
77///    `"sandbox"`, that backend is forced.
78/// 2. On macOS, buildah is tried first; if unavailable, the sandbox backend
79///    is returned as a fallback.
80/// 3. On Linux, only the buildah backend is available.
81///
82/// # Errors
83///
84/// Returns an error if no usable backend can be found.
85pub async fn detect_backend() -> Result<Arc<dyn BuildBackend>> {
86    // Check for explicit override
87    if let Ok(forced) = std::env::var("ZLAYER_BACKEND") {
88        match forced.to_lowercase().as_str() {
89            "buildah" => {
90                let backend = BuildahBackend::new().await?;
91                return Ok(Arc::new(backend));
92            }
93            #[cfg(target_os = "macos")]
94            "sandbox" => {
95                let backend = SandboxBackend::default();
96                return Ok(Arc::new(backend));
97            }
98            other => {
99                return Err(BuildError::BuildahNotFound {
100                    message: format!("Unknown ZLAYER_BACKEND value: {other}"),
101                });
102            }
103        }
104    }
105
106    // Auto-detect
107    #[cfg(target_os = "macos")]
108    {
109        if let Ok(backend) = BuildahBackend::try_new().await {
110            Ok(Arc::new(backend))
111        } else {
112            tracing::info!("Buildah not available on macOS, falling back to sandbox backend");
113            Ok(Arc::new(SandboxBackend::default()))
114        }
115    }
116
117    #[cfg(not(target_os = "macos"))]
118    {
119        let backend = BuildahBackend::new().await?;
120        Ok(Arc::new(backend))
121    }
122}