Skip to main content

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_ref());
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 buildah;
187pub mod builder;
188pub mod dockerfile;
189pub mod error;
190pub mod pipeline;
191pub mod templates;
192pub mod tui;
193pub mod wasm_builder;
194pub mod zimage;
195
196// Re-export main types at crate root
197pub use buildah::{
198    current_platform,
199    install_instructions,
200    is_platform_supported,
201    BuildahCommand,
202    BuildahExecutor,
203    // Installation types
204    BuildahInstallation,
205    BuildahInstaller,
206    CommandOutput,
207    InstallError,
208};
209#[cfg(feature = "cache")]
210pub use builder::CacheBackendConfig;
211pub use builder::{BuildOptions, BuiltImage, ImageBuilder, RegistryAuth};
212pub use dockerfile::{
213    expand_variables,
214    // Instruction types
215    AddInstruction,
216    ArgInstruction,
217    CopyInstruction,
218    Dockerfile,
219    EnvInstruction,
220    ExposeInstruction,
221    ExposeProtocol,
222    HealthcheckInstruction,
223    ImageRef,
224    Instruction,
225    RunInstruction,
226    ShellOrExec,
227    Stage,
228    // Variable expansion
229    VariableContext,
230};
231pub use error::{BuildError, Result};
232pub use templates::{
233    detect_runtime, detect_runtime_with_version, get_template, get_template_by_name,
234    list_templates, resolve_runtime, Runtime, RuntimeInfo,
235};
236pub use tui::{BuildEvent, BuildTui, InstructionStatus, PlainLogger};
237
238// Pipeline re-exports
239pub use pipeline::{
240    parse_pipeline, PipelineDefaults, PipelineExecutor, PipelineImage, PipelineResult, PushConfig,
241    ZPipeline,
242};
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_parse_and_convert_simple() {
250        let dockerfile = Dockerfile::parse(
251            r#"
252FROM alpine:3.18
253RUN echo "hello"
254COPY . /app
255WORKDIR /app
256"#,
257        )
258        .unwrap();
259
260        assert_eq!(dockerfile.stages.len(), 1);
261
262        let stage = &dockerfile.stages[0];
263        assert_eq!(stage.instructions.len(), 3);
264
265        // Convert each instruction to buildah commands
266        let container = "test-container";
267        for instruction in &stage.instructions {
268            let cmds = BuildahCommand::from_instruction(container, instruction);
269            assert!(!cmds.is_empty() || matches!(instruction, Instruction::Arg(_)));
270        }
271    }
272
273    #[test]
274    fn test_parse_multistage_and_convert() {
275        let dockerfile = Dockerfile::parse(
276            r#"
277FROM golang:1.21 AS builder
278WORKDIR /src
279COPY . .
280RUN go build -o /app
281
282FROM alpine:3.18
283COPY --from=builder /app /app
284ENTRYPOINT ["/app"]
285"#,
286        )
287        .unwrap();
288
289        assert_eq!(dockerfile.stages.len(), 2);
290
291        // First stage (builder)
292        let builder = &dockerfile.stages[0];
293        assert_eq!(builder.name, Some("builder".to_string()));
294        assert_eq!(builder.instructions.len(), 3);
295
296        // Second stage (runtime)
297        let runtime = &dockerfile.stages[1];
298        assert!(runtime.name.is_none());
299        assert_eq!(runtime.instructions.len(), 2);
300
301        // Check COPY --from=builder is preserved
302        if let Instruction::Copy(copy) = &runtime.instructions[0] {
303            assert_eq!(copy.from, Some("builder".to_string()));
304        } else {
305            panic!("Expected COPY instruction");
306        }
307    }
308
309    #[test]
310    fn test_variable_expansion() {
311        let mut ctx = VariableContext::new();
312        ctx.add_arg("VERSION", Some("1.0".to_string()));
313        ctx.set_env("HOME", "/app".to_string());
314
315        assert_eq!(ctx.expand("$VERSION"), "1.0");
316        assert_eq!(ctx.expand("$HOME"), "/app");
317        assert_eq!(ctx.expand("${UNSET:-default}"), "default");
318    }
319
320    #[test]
321    fn test_buildah_command_generation() {
322        // Test various instruction conversions
323        let container = "test";
324
325        // RUN
326        let run = Instruction::Run(RunInstruction {
327            command: ShellOrExec::Shell("apt-get update".to_string()),
328            mounts: vec![],
329            network: None,
330            security: None,
331        });
332        let cmds = BuildahCommand::from_instruction(container, &run);
333        assert_eq!(cmds.len(), 1);
334        assert!(cmds[0].args.contains(&"run".to_string()));
335
336        // ENV
337        let mut vars = std::collections::HashMap::new();
338        vars.insert("PATH".to_string(), "/usr/local/bin".to_string());
339        let env = Instruction::Env(EnvInstruction { vars });
340        let cmds = BuildahCommand::from_instruction(container, &env);
341        assert_eq!(cmds.len(), 1);
342        assert!(cmds[0].args.contains(&"config".to_string()));
343        assert!(cmds[0].args.contains(&"--env".to_string()));
344
345        // WORKDIR
346        let workdir = Instruction::Workdir("/app".to_string());
347        let cmds = BuildahCommand::from_instruction(container, &workdir);
348        assert_eq!(cmds.len(), 1);
349        assert!(cmds[0].args.contains(&"--workingdir".to_string()));
350    }
351}