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}