zlayer_builder/lib.rs
1//! `ZLayer` Builder - Dockerfile parsing, `ZImagefile` support, and buildah command generation
2//!
3//! This crate provides functionality for parsing Dockerfiles (and `ZImagefiles`),
4//! converting them into buildah commands for container image building. It is
5//! designed to be used as part of the `ZLayer` container orchestration platform.
6//!
7//! # Architecture
8//!
9//! The crate is organized into several modules:
10//!
11//! - [`dockerfile`]: Types and parsing for Dockerfile content
12//! - [`buildah`]: Command generation and execution for buildah
13//! - [`builder`]: High-level [`ImageBuilder`] API for orchestrating builds
14//! - [`zimage`]: `ZImagefile` (YAML-based) parsing and Dockerfile conversion
15//! - [`tui`]: Terminal UI for build progress visualization
16//! - [`templates`]: Runtime templates for common development environments
17//! - [`error`]: Error types for the builder subsystem
18//!
19//! # Quick Start with `ImageBuilder`
20//!
21//! The recommended way to build images is using the [`ImageBuilder`] API:
22//!
23//! ```no_run
24//! use zlayer_builder::{ImageBuilder, Runtime};
25//!
26//! #[tokio::main]
27//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
28//! // Build from a Dockerfile
29//! let image = ImageBuilder::new("./my-app").await?
30//! .tag("myapp:latest")
31//! .tag("myapp:v1.0.0")
32//! .build()
33//! .await?;
34//!
35//! println!("Built image: {}", image.image_id);
36//! Ok(())
37//! }
38//! ```
39//!
40//! # Using Runtime Templates
41//!
42//! Build images without writing a Dockerfile using runtime templates:
43//!
44//! ```no_run
45//! use zlayer_builder::{ImageBuilder, Runtime};
46//!
47//! #[tokio::main]
48//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
49//! // Auto-detect runtime from project files, or specify explicitly
50//! let image = ImageBuilder::new("./my-node-app").await?
51//! .runtime(Runtime::Node20)
52//! .tag("myapp:latest")
53//! .build()
54//! .await?;
55//!
56//! Ok(())
57//! }
58//! ```
59//!
60//! # Building from a `ZImagefile`
61//!
62//! `ZImagefiles` are a YAML-based alternative to Dockerfiles. The builder
63//! auto-detects a file named `ZImagefile` in the context directory, or you
64//! can point to one explicitly with [`ImageBuilder::zimagefile`]:
65//!
66//! ```no_run
67//! use zlayer_builder::ImageBuilder;
68//!
69//! #[tokio::main]
70//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
71//! let image = ImageBuilder::new("./my-app").await?
72//! .zimagefile("./my-app/ZImagefile")
73//! .tag("myapp:latest")
74//! .build()
75//! .await?;
76//!
77//! Ok(())
78//! }
79//! ```
80//!
81//! `ZImagefiles` support four build modes: runtime template shorthand,
82//! single-stage (`base` + `steps`), multi-stage (`stages` map), and WASM.
83//! See the [`zimage`] module for the full type definitions.
84//!
85//! # Low-Level API
86//!
87//! For more control, you can use the low-level Dockerfile parsing and
88//! buildah command generation APIs directly:
89//!
90//! ```no_run
91//! use zlayer_builder::{Dockerfile, BuildahCommand, BuildahExecutor};
92//!
93//! # async fn example() -> Result<(), zlayer_builder::BuildError> {
94//! // Parse a Dockerfile
95//! let dockerfile = Dockerfile::parse(r#"
96//! FROM alpine:3.18
97//! RUN apk add --no-cache curl
98//! COPY . /app
99//! WORKDIR /app
100//! CMD ["./app"]
101//! "#)?;
102//!
103//! // Get the final stage
104//! let stage = dockerfile.final_stage().unwrap();
105//!
106//! // Create buildah commands for each instruction
107//! let executor = BuildahExecutor::new()?;
108//!
109//! // Create a working container from the base image
110//! let from_cmd = BuildahCommand::from_image(&stage.base_image.to_string());
111//! let output = executor.execute_checked(&from_cmd).await?;
112//! let container_id = output.stdout.trim();
113//!
114//! // Execute each instruction
115//! for instruction in &stage.instructions {
116//! let cmds = BuildahCommand::from_instruction(container_id, instruction);
117//! for cmd in cmds {
118//! executor.execute_checked(&cmd).await?;
119//! }
120//! }
121//!
122//! // Commit the container to create an image
123//! let commit_cmd = BuildahCommand::commit(container_id, "myimage:latest");
124//! executor.execute_checked(&commit_cmd).await?;
125//!
126//! // Clean up the working container
127//! let rm_cmd = BuildahCommand::rm(container_id);
128//! executor.execute(&rm_cmd).await?;
129//!
130//! # Ok(())
131//! # }
132//! ```
133//!
134//! # Features
135//!
136//! ## `ImageBuilder` (High-Level API)
137//!
138//! The [`ImageBuilder`] provides a fluent API for:
139//!
140//! - Building from Dockerfiles or runtime templates
141//! - Multi-stage builds with target stage selection
142//! - Build arguments (ARG values)
143//! - Image tagging and registry pushing
144//! - TUI progress updates via event channels
145//!
146//! ## Dockerfile Parsing
147//!
148//! The crate supports parsing standard Dockerfiles with:
149//!
150//! - Multi-stage builds with named stages
151//! - All standard Dockerfile instructions (FROM, RUN, COPY, ADD, ENV, etc.)
152//! - ARG/ENV variable expansion with default values
153//! - Global ARGs (before first FROM)
154//!
155//! ## Buildah Integration
156//!
157//! Commands are generated for buildah, a daemon-less container builder:
158//!
159//! - Container creation from images or scratch
160//! - Running commands (shell and exec form)
161//! - Copying files (including from other stages)
162//! - Configuration (env, workdir, entrypoint, cmd, labels, etc.)
163//! - Committing containers to images
164//! - Image tagging and pushing
165//!
166//! ## Runtime Templates
167//!
168//! Pre-built templates for common development environments:
169//!
170//! - Node.js 20/22 (Alpine-based, production optimized)
171//! - Python 3.12/3.13 (Slim Debian-based)
172//! - Rust (Static musl binary)
173//! - Go (Static binary)
174//! - Deno and Bun
175//!
176//! ## Variable Expansion
177//!
178//! Full support for Dockerfile variable syntax:
179//!
180//! - `$VAR` and `${VAR}` - Simple variable reference
181//! - `${VAR:-default}` - Default if unset or empty
182//! - `${VAR:+alternate}` - Alternate if set and non-empty
183//! - `${VAR-default}` - Default only if unset
184//! - `${VAR+alternate}` - Alternate if set (including empty)
185
186pub mod backend;
187pub mod buildah;
188pub mod builder;
189pub mod dockerfile;
190pub mod error;
191pub mod harvest;
192#[cfg(target_os = "macos")]
193pub mod macos_compat;
194#[cfg(target_os = "macos")]
195pub mod macos_image_resolver;
196#[cfg(target_os = "macos")]
197pub mod macos_toolchain;
198/// OCI image-layout archive assembly (used by non-buildah backends to feed the
199/// local-registry import). Two non-test callers: the `cfg(macos)`
200/// `SandboxBackend` (paired with `cache`, since it sits on the registry path)
201/// and the `cfg(windows)` native HCS builder's buildah-free
202/// `export_built_image_to_oci_archive`. Compiled on macOS-with-cache (where the
203/// sandbox uses it), on every Windows build (where the HCS export uses it), and
204/// in any test build (so the assembly is unit-tested off both).
205#[cfg(any(
206 all(feature = "cache", target_os = "macos"),
207 target_os = "windows",
208 test
209))]
210mod oci_archive;
211pub mod pipeline;
212#[cfg(target_os = "macos")]
213pub mod sandbox_builder;
214pub mod templates;
215pub mod tui;
216pub mod wasm_builder;
217pub mod windows;
218pub mod windows_builder;
219pub mod windows_image_resolver;
220// Inner `#![cfg(target_os = "windows")]` in the module gates the body; declare
221// it unconditionally here (like `windows_builder`) so a redundant cfg isn't
222// applied twice.
223pub mod windows_toolchain;
224pub mod zimage;
225
226// Re-export main types at crate root
227pub use buildah::{
228 current_platform,
229 install_instructions,
230 is_platform_supported,
231 BuildahCommand,
232 BuildahExecutor,
233 // Installation types
234 BuildahInstallation,
235 BuildahInstaller,
236 CommandOutput,
237 // OS-aware Dockerfile translator, shared by the buildah backend and the
238 // Phase L-4 HCS (Windows) backend.
239 DockerfileTranslator,
240 InstallError,
241};
242#[cfg(feature = "cache")]
243pub use builder::CacheBackendConfig;
244pub use builder::{
245 find_context_zimagefile, BuildOptions, BuildOutput, BuiltImage, ImageBuilder, PullBaseMode,
246 RegistryAuth,
247};
248// Re-export the registry types a caller needs to wire the daemon's already-open
249// image store into a build (`ImageBuilder::with_local_registry_arc` +
250// `with_cache_backend`). The Docker-compat socket build path (in `zlayer-docker`,
251// which depends on `zlayer-builder` but NOT on `zlayer-registry`) names these to
252// import socket-built images into the live store instead of a second handle.
253pub use dockerfile::{
254 expand_variables,
255 // Instruction types
256 AddInstruction,
257 ArgInstruction,
258 CopyInstruction,
259 Dockerfile,
260 EnvInstruction,
261 ExposeInstruction,
262 ExposeProtocol,
263 HealthcheckInstruction,
264 Instruction,
265 RunInstruction,
266 ShellOrExec,
267 Stage,
268 // Variable expansion
269 VariableContext,
270};
271pub use error::{BuildError, Result};
272pub use templates::{
273 detect_runtime, detect_runtime_with_version, get_template, get_template_by_name,
274 list_templates, resolve_runtime, Runtime, RuntimeInfo,
275};
276pub use tui::{BuildEvent, BuildTui, InstructionStatus, PlainLogger};
277#[cfg(feature = "cache")]
278pub use zlayer_registry::cache::BlobCacheBackend;
279#[cfg(feature = "local-registry")]
280pub use zlayer_registry::LocalRegistry;
281
282// macOS sandbox builder re-exports
283#[cfg(target_os = "macos")]
284pub use sandbox_builder::{SandboxBuildResult, SandboxImageBuilder, SandboxImageConfig};
285
286// Pipeline re-exports
287pub use pipeline::{
288 parse_pipeline, PipelineCacheConfig, PipelineDefaults, PipelineExecutor, PipelineImage,
289 PipelineResult, PushConfig, ZPipeline,
290};
291
292// Backend re-exports
293#[cfg(target_os = "macos")]
294pub use backend::SandboxBackend;
295pub use backend::{detect_backend, BuildBackend, BuildahBackend, ImageOs, ImageOsParseError};
296
297// Build-context OS auto-detection (classifies the image OS from the entrypoint
298// binary's magic bytes when no explicit `os:`/`platform:` is declared).
299pub use zimage::detect_image_os_from_binary;
300
301/// Process-wide lock shared by every test in this crate that mutates
302/// environment variables (`PATH`, `ZLAYER_BUILDD_BIN`, etc.). Cargo runs
303/// tests from a single crate in the same process by default, so any two
304/// env-mutating tests that don't share a lock will race. New env-mutating
305/// tests in this crate MUST acquire this mutex before touching env state.
306#[cfg(test)]
307pub(crate) static TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn test_parse_and_convert_simple() {
315 let dockerfile = Dockerfile::parse(
316 r#"
317FROM alpine:3.18
318RUN echo "hello"
319COPY . /app
320WORKDIR /app
321"#,
322 )
323 .unwrap();
324
325 assert_eq!(dockerfile.stages.len(), 1);
326
327 let stage = &dockerfile.stages[0];
328 assert_eq!(stage.instructions.len(), 3);
329
330 // Convert each instruction to buildah commands
331 let container = "test-container";
332 for instruction in &stage.instructions {
333 let cmds = BuildahCommand::from_instruction(container, instruction);
334 assert!(!cmds.is_empty() || matches!(instruction, Instruction::Arg(_)));
335 }
336 }
337
338 #[test]
339 fn test_parse_multistage_and_convert() {
340 let dockerfile = Dockerfile::parse(
341 r#"
342FROM golang:1.21 AS builder
343WORKDIR /src
344COPY . .
345RUN go build -o /app
346
347FROM alpine:3.18
348COPY --from=builder /app /app
349ENTRYPOINT ["/app"]
350"#,
351 )
352 .unwrap();
353
354 assert_eq!(dockerfile.stages.len(), 2);
355
356 // First stage (builder)
357 let builder = &dockerfile.stages[0];
358 assert_eq!(builder.name, Some("builder".to_string()));
359 assert_eq!(builder.instructions.len(), 3);
360
361 // Second stage (runtime)
362 let runtime = &dockerfile.stages[1];
363 assert!(runtime.name.is_none());
364 assert_eq!(runtime.instructions.len(), 2);
365
366 // Check COPY --from=builder is preserved
367 if let Instruction::Copy(copy) = &runtime.instructions[0] {
368 assert_eq!(copy.from, Some("builder".to_string()));
369 } else {
370 panic!("Expected COPY instruction");
371 }
372 }
373
374 #[test]
375 fn test_variable_expansion() {
376 let mut ctx = VariableContext::new();
377 ctx.add_arg("VERSION", Some("1.0".to_string()));
378 ctx.set_env("HOME", "/app".to_string());
379
380 assert_eq!(ctx.expand("$VERSION"), "1.0");
381 assert_eq!(ctx.expand("$HOME"), "/app");
382 assert_eq!(ctx.expand("${UNSET:-default}"), "default");
383 }
384
385 #[test]
386 fn test_buildah_command_generation() {
387 // Test various instruction conversions
388 let container = "test";
389
390 // RUN
391 let run = Instruction::Run(RunInstruction {
392 command: ShellOrExec::Shell("apt-get update".to_string()),
393 mounts: vec![],
394 network: None,
395 security: None,
396 env: std::collections::HashMap::new(),
397 });
398 let cmds = BuildahCommand::from_instruction(container, &run);
399 assert_eq!(cmds.len(), 1);
400 assert!(cmds[0].args.contains(&"run".to_string()));
401
402 // ENV
403 let mut vars = std::collections::HashMap::new();
404 vars.insert("PATH".to_string(), "/usr/local/bin".to_string());
405 let env = Instruction::Env(EnvInstruction { vars });
406 let cmds = BuildahCommand::from_instruction(container, &env);
407 assert_eq!(cmds.len(), 1);
408 assert!(cmds[0].args.contains(&"config".to_string()));
409 assert!(cmds[0].args.contains(&"--env".to_string()));
410
411 // WORKDIR materialises the directory (mkdir -p) AND records it as
412 // the working directory (config --workingdir), matching Docker's
413 // WORKDIR semantics.
414 let workdir = Instruction::Workdir("/app".to_string());
415 let cmds = BuildahCommand::from_instruction(container, &workdir);
416 assert_eq!(cmds.len(), 2);
417 assert!(cmds
418 .iter()
419 .any(|c| c.args.contains(&"run".to_string()) && c.args.contains(&"mkdir".to_string())));
420 assert!(cmds
421 .iter()
422 .any(|c| c.args.contains(&"--workingdir".to_string())));
423 }
424}