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//! - `HcsBackend` (Windows-only, see [`hcs`]) — native Windows builder via HCS;
12//!   wraps `zlayer_agent::windows::{scratch, layer}` to produce OCI images
13//!   without Docker Desktop.
14//!
15//! # Target OS routing
16//!
17//! The [`ImageOs`] enum selects which *image* OS we are building for (Linux or
18//! Windows). [`detect_backend`] branches on both the host OS and the target OS:
19//! Windows images can only be built on a Windows host (via the HCS-backed
20//! backend that landed in Phase L-4), while Linux images on Windows hosts
21//! currently require a Linux peer (a WSL2-buildah route is a Phase L
22//! follow-up).
23
24mod buildah;
25#[cfg(target_os = "windows")]
26pub mod hcs;
27#[cfg(target_os = "macos")]
28mod sandbox;
29
30pub use buildah::BuildahBackend;
31#[cfg(target_os = "windows")]
32pub use hcs::HcsBackend;
33#[cfg(target_os = "macos")]
34pub use sandbox::SandboxBackend;
35
36use std::path::Path;
37use std::sync::Arc;
38
39use crate::builder::{BuildOptions, BuiltImage, RegistryAuth};
40use crate::dockerfile::Dockerfile;
41use crate::error::{BuildError, Result};
42use crate::tui::BuildEvent;
43
44/// Operating system of the image being built.
45///
46/// This is distinct from the host OS — a Linux host can only build Linux
47/// images, a Windows host is required to build Windows images (via the
48/// HCS-backed backend landing in Phase L-4).
49///
50/// Serializes to lowercase (`"linux"` / `"windows"`) for YAML/JSON configs.
51#[derive(
52    Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
53)]
54#[serde(rename_all = "lowercase")]
55pub enum ImageOs {
56    #[default]
57    Linux,
58    Windows,
59}
60
61/// Error returned when parsing an unknown [`ImageOs`] string.
62#[derive(thiserror::Error, Debug)]
63#[error("unknown OS: {0} (expected linux or windows)")]
64pub struct ImageOsParseError(pub String);
65
66impl std::str::FromStr for ImageOs {
67    type Err = ImageOsParseError;
68
69    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
70        // Accept bare OS names ("linux", "Windows") AND platform-style strings
71        // ("linux/amd64", "windows/arm64"). Split on '/' first and match the
72        // OS component case-insensitively.
73        let os_part = s.split('/').next().unwrap_or("").trim();
74        match os_part.to_ascii_lowercase().as_str() {
75            "linux" => Ok(ImageOs::Linux),
76            "windows" => Ok(ImageOs::Windows),
77            _ => Err(ImageOsParseError(s.to_string())),
78        }
79    }
80}
81
82/// A pluggable build backend.
83///
84/// Implementations handle the low-level mechanics of building, pushing, tagging,
85/// and managing manifest lists for container images.
86#[async_trait::async_trait]
87pub trait BuildBackend: Send + Sync {
88    /// Build a container image from a parsed Dockerfile.
89    ///
90    /// # Arguments
91    ///
92    /// * `context`    — path to the build context directory
93    /// * `dockerfile` — parsed Dockerfile IR
94    /// * `options`    — build configuration (tags, args, caching, etc.)
95    /// * `event_tx`   — optional channel for streaming progress events to a TUI
96    async fn build_image(
97        &self,
98        context: &Path,
99        dockerfile: &Dockerfile,
100        options: &BuildOptions,
101        event_tx: Option<std::sync::mpsc::Sender<BuildEvent>>,
102    ) -> Result<BuiltImage>;
103
104    /// Push an image to a container registry.
105    async fn push_image(&self, tag: &str, auth: Option<&RegistryAuth>) -> Result<()>;
106
107    /// Tag an existing image with a new name.
108    async fn tag_image(&self, image: &str, new_tag: &str) -> Result<()>;
109
110    /// Create a new (empty) manifest list.
111    async fn manifest_create(&self, name: &str) -> Result<()>;
112
113    /// Add an image to an existing manifest list.
114    async fn manifest_add(&self, manifest: &str, image: &str) -> Result<()>;
115
116    /// Push a manifest list (and all referenced images) to a registry.
117    async fn manifest_push(&self, name: &str, destination: &str) -> Result<()>;
118
119    /// Returns `true` if the backend tooling is installed and functional.
120    async fn is_available(&self) -> bool;
121
122    /// Human-readable name for this backend (e.g. `"buildah"`, `"sandbox"`).
123    fn name(&self) -> &'static str;
124}
125
126/// Auto-detect the best available build backend for the given target OS.
127///
128/// Selection matrix (host × target):
129///
130/// | Host / Target | Linux image                               | Windows image                             |
131/// |---------------|-------------------------------------------|-------------------------------------------|
132/// | Linux         | buildah                                   | Err — requires Windows host               |
133/// | macOS         | buildah (if available) else macos-sandbox | Err — requires Windows host               |
134/// | Windows       | Err — Linux peer required (WSL2 follow-up)| HCS-backed native Windows builder (L-4)   |
135///
136/// If the `ZLAYER_BACKEND` env var is set to `"buildah"` or (on macOS)
137/// `"sandbox"`, that backend is forced regardless of target OS.
138///
139/// # Errors
140///
141/// Returns an error if the host cannot build images for the requested
142/// `target_os`, or if the selected backend's tooling is missing.
143pub async fn detect_backend(target_os: ImageOs) -> Result<Arc<dyn BuildBackend>> {
144    // Check for explicit override first — respected regardless of target_os so
145    // devs can force a backend during debugging.
146    if let Ok(forced) = std::env::var("ZLAYER_BACKEND") {
147        match forced.to_lowercase().as_str() {
148            "buildah" => {
149                let backend = BuildahBackend::new().await?;
150                return Ok(Arc::new(backend));
151            }
152            #[cfg(target_os = "macos")]
153            "sandbox" => {
154                let backend = SandboxBackend::default();
155                return Ok(Arc::new(backend));
156            }
157            other => {
158                return Err(BuildError::BuildahNotFound {
159                    message: format!("Unknown ZLAYER_BACKEND value: {other}"),
160                });
161            }
162        }
163    }
164
165    // Host × target routing.
166    #[cfg(target_os = "windows")]
167    {
168        match target_os {
169            ImageOs::Linux => Err(BuildError::BuildahNotFound {
170                message: "Linux image building on Windows hosts requires a Linux peer \
171                          (Phase L follow-up will add WSL2-buildah routing)"
172                    .to_string(),
173            }),
174            ImageOs::Windows => {
175                let backend = HcsBackend::new().await?;
176                Ok(Arc::new(backend))
177            }
178        }
179    }
180
181    #[cfg(target_os = "macos")]
182    {
183        match target_os {
184            ImageOs::Linux => {
185                if let Ok(backend) = BuildahBackend::try_new().await {
186                    Ok(Arc::new(backend))
187                } else {
188                    tracing::info!(
189                        "Buildah not available on macOS, falling back to sandbox backend"
190                    );
191                    Ok(Arc::new(SandboxBackend::default()))
192                }
193            }
194            ImageOs::Windows => Err(BuildError::BuildahNotFound {
195                message: "building Windows images requires a Windows host — run this build \
196                          on a Windows node of the ZLayer cluster"
197                    .to_string(),
198            }),
199        }
200    }
201
202    #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
203    {
204        match target_os {
205            ImageOs::Linux => {
206                let backend = BuildahBackend::new().await?;
207                Ok(Arc::new(backend))
208            }
209            ImageOs::Windows => Err(BuildError::BuildahNotFound {
210                message: "building Windows images requires a Windows host — run this build \
211                          on a Windows node of the ZLayer cluster"
212                    .to_string(),
213            }),
214        }
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn image_os_parses_simple_and_slash_form() {
224        assert_eq!("linux".parse::<ImageOs>().unwrap(), ImageOs::Linux);
225        assert_eq!("Linux".parse::<ImageOs>().unwrap(), ImageOs::Linux);
226        assert_eq!("windows".parse::<ImageOs>().unwrap(), ImageOs::Windows);
227        assert_eq!("linux/amd64".parse::<ImageOs>().unwrap(), ImageOs::Linux);
228        assert_eq!(
229            "windows/amd64".parse::<ImageOs>().unwrap(),
230            ImageOs::Windows
231        );
232        assert!("darwin".parse::<ImageOs>().is_err());
233    }
234}