Skip to main content

zlayer_builder/templates/
mod.rs

1//! Runtime templates for ZLayer builder
2//!
3//! This module provides pre-built Dockerfile templates for common runtimes,
4//! allowing users to build container images without writing Dockerfiles.
5//!
6//! # Usage
7//!
8//! Templates can be used via the `zlayer build` command:
9//!
10//! ```bash
11//! # Use a specific runtime template
12//! zlayer build --runtime node20
13//!
14//! # Auto-detect runtime from project files
15//! zlayer build --detect-runtime
16//! ```
17//!
18//! # Available Runtimes
19//!
20//! - **Node.js 20** (`node20`): Production-ready Node.js 20 with Alpine base
21//! - **Node.js 22** (`node22`): Production-ready Node.js 22 with Alpine base
22//! - **Python 3.12** (`python312`): Python 3.12 slim with pip packages
23//! - **Python 3.13** (`python313`): Python 3.13 slim with pip packages
24//! - **Rust** (`rust`): Static binary build with musl
25//! - **Go** (`go`): Static binary build with Alpine
26//! - **Deno** (`deno`): Official Deno runtime
27//! - **Bun** (`bun`): Official Bun runtime
28//!
29//! # Auto-Detection
30//!
31//! The [`detect_runtime`] function can automatically detect the appropriate
32//! runtime based on files present in the project directory:
33//!
34//! - `package.json` -> Node.js (unless Bun or Deno indicators present)
35//! - `bun.lockb` -> Bun
36//! - `deno.json` or `deno.jsonc` -> Deno
37//! - `Cargo.toml` -> Rust
38//! - `requirements.txt`, `pyproject.toml`, `setup.py` -> Python
39//! - `go.mod` -> Go
40
41mod detect;
42
43use std::fmt;
44use std::path::Path;
45use std::str::FromStr;
46
47pub use detect::{detect_runtime, detect_runtime_with_version};
48
49/// Supported runtime environments
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum Runtime {
52    /// Node.js 20 (LTS)
53    Node20,
54    /// Node.js 22 (Current)
55    Node22,
56    /// Python 3.12
57    Python312,
58    /// Python 3.13
59    Python313,
60    /// Rust (latest stable)
61    Rust,
62    /// Go (latest stable)
63    Go,
64    /// Deno (latest)
65    Deno,
66    /// Bun (latest)
67    Bun,
68}
69
70impl Runtime {
71    /// Get all available runtimes
72    pub fn all() -> &'static [RuntimeInfo] {
73        &[
74            RuntimeInfo {
75                runtime: Runtime::Node20,
76                name: "node20",
77                description: "Node.js 20 (LTS) - Alpine-based, production optimized",
78                detect_files: &["package.json"],
79            },
80            RuntimeInfo {
81                runtime: Runtime::Node22,
82                name: "node22",
83                description: "Node.js 22 (Current) - Alpine-based, production optimized",
84                detect_files: &["package.json"],
85            },
86            RuntimeInfo {
87                runtime: Runtime::Python312,
88                name: "python312",
89                description: "Python 3.12 - Slim Debian-based with pip",
90                detect_files: &["requirements.txt", "pyproject.toml", "setup.py"],
91            },
92            RuntimeInfo {
93                runtime: Runtime::Python313,
94                name: "python313",
95                description: "Python 3.13 - Slim Debian-based with pip",
96                detect_files: &["requirements.txt", "pyproject.toml", "setup.py"],
97            },
98            RuntimeInfo {
99                runtime: Runtime::Rust,
100                name: "rust",
101                description: "Rust - Static musl binary, minimal Alpine runtime",
102                detect_files: &["Cargo.toml"],
103            },
104            RuntimeInfo {
105                runtime: Runtime::Go,
106                name: "go",
107                description: "Go - Static binary, minimal Alpine runtime",
108                detect_files: &["go.mod"],
109            },
110            RuntimeInfo {
111                runtime: Runtime::Deno,
112                name: "deno",
113                description: "Deno - Official runtime with TypeScript support",
114                detect_files: &["deno.json", "deno.jsonc"],
115            },
116            RuntimeInfo {
117                runtime: Runtime::Bun,
118                name: "bun",
119                description: "Bun - Fast JavaScript runtime and bundler",
120                detect_files: &["bun.lockb"],
121            },
122        ]
123    }
124
125    /// Parse a runtime from its name
126    pub fn from_name(name: &str) -> Option<Runtime> {
127        let name_lower = name.to_lowercase();
128        match name_lower.as_str() {
129            "node20" | "node-20" | "nodejs20" | "node" => Some(Runtime::Node20),
130            "node22" | "node-22" | "nodejs22" => Some(Runtime::Node22),
131            "python312" | "python-312" | "python3.12" | "python" => Some(Runtime::Python312),
132            "python313" | "python-313" | "python3.13" => Some(Runtime::Python313),
133            "rust" | "rs" => Some(Runtime::Rust),
134            "go" | "golang" => Some(Runtime::Go),
135            "deno" => Some(Runtime::Deno),
136            "bun" => Some(Runtime::Bun),
137            _ => None,
138        }
139    }
140
141    /// Get information about this runtime
142    pub fn info(&self) -> &'static RuntimeInfo {
143        Runtime::all()
144            .iter()
145            .find(|info| info.runtime == *self)
146            .expect("All runtimes must have info")
147    }
148
149    /// Get the Dockerfile template for this runtime
150    pub fn template(&self) -> &'static str {
151        match self {
152            Runtime::Node20 => include_str!("dockerfiles/node20.Dockerfile"),
153            Runtime::Node22 => include_str!("dockerfiles/node22.Dockerfile"),
154            Runtime::Python312 => include_str!("dockerfiles/python312.Dockerfile"),
155            Runtime::Python313 => include_str!("dockerfiles/python313.Dockerfile"),
156            Runtime::Rust => include_str!("dockerfiles/rust.Dockerfile"),
157            Runtime::Go => include_str!("dockerfiles/go.Dockerfile"),
158            Runtime::Deno => include_str!("dockerfiles/deno.Dockerfile"),
159            Runtime::Bun => include_str!("dockerfiles/bun.Dockerfile"),
160        }
161    }
162
163    /// Get the canonical name for this runtime
164    pub fn name(&self) -> &'static str {
165        self.info().name
166    }
167}
168
169impl fmt::Display for Runtime {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        write!(f, "{}", self.name())
172    }
173}
174
175impl FromStr for Runtime {
176    type Err = String;
177
178    fn from_str(s: &str) -> Result<Self, Self::Err> {
179        Runtime::from_name(s).ok_or_else(|| format!("Unknown runtime: {}", s))
180    }
181}
182
183/// Information about a runtime template
184#[derive(Debug, Clone, Copy)]
185pub struct RuntimeInfo {
186    /// The runtime enum value
187    pub runtime: Runtime,
188    /// Short name used in CLI (e.g., "node20")
189    pub name: &'static str,
190    /// Human-readable description
191    pub description: &'static str,
192    /// Files that indicate this runtime should be used
193    pub detect_files: &'static [&'static str],
194}
195
196/// List all available templates
197pub fn list_templates() -> Vec<&'static RuntimeInfo> {
198    Runtime::all().iter().collect()
199}
200
201/// Get template content for a runtime
202pub fn get_template(runtime: Runtime) -> &'static str {
203    runtime.template()
204}
205
206/// Get template content by runtime name
207pub fn get_template_by_name(name: &str) -> Option<&'static str> {
208    Runtime::from_name(name).map(|r| r.template())
209}
210
211/// Resolve runtime from either explicit name or auto-detection
212pub fn resolve_runtime(
213    runtime_name: Option<&str>,
214    context_path: impl AsRef<Path>,
215    use_version_hints: bool,
216) -> Option<Runtime> {
217    // If explicitly specified, use that
218    if let Some(name) = runtime_name {
219        return Runtime::from_name(name);
220    }
221
222    // Otherwise, auto-detect
223    if use_version_hints {
224        detect_runtime_with_version(context_path)
225    } else {
226        detect_runtime(context_path)
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::Dockerfile;
234    use std::fs;
235    use tempfile::TempDir;
236
237    #[test]
238    fn test_runtime_from_name() {
239        assert_eq!(Runtime::from_name("node20"), Some(Runtime::Node20));
240        assert_eq!(Runtime::from_name("Node20"), Some(Runtime::Node20));
241        assert_eq!(Runtime::from_name("node"), Some(Runtime::Node20));
242        assert_eq!(Runtime::from_name("python"), Some(Runtime::Python312));
243        assert_eq!(Runtime::from_name("rust"), Some(Runtime::Rust));
244        assert_eq!(Runtime::from_name("go"), Some(Runtime::Go));
245        assert_eq!(Runtime::from_name("golang"), Some(Runtime::Go));
246        assert_eq!(Runtime::from_name("deno"), Some(Runtime::Deno));
247        assert_eq!(Runtime::from_name("bun"), Some(Runtime::Bun));
248        assert_eq!(Runtime::from_name("unknown"), None);
249    }
250
251    #[test]
252    fn test_runtime_info() {
253        let info = Runtime::Node20.info();
254        assert_eq!(info.name, "node20");
255        assert!(info.description.contains("Node.js"));
256        assert!(info.detect_files.contains(&"package.json"));
257    }
258
259    #[test]
260    fn test_all_templates_parse_correctly() {
261        for info in Runtime::all() {
262            let template = info.runtime.template();
263            let result = Dockerfile::parse(template);
264            assert!(
265                result.is_ok(),
266                "Template {} failed to parse: {:?}",
267                info.name,
268                result.err()
269            );
270
271            let dockerfile = result.unwrap();
272            assert!(
273                !dockerfile.stages.is_empty(),
274                "Template {} has no stages",
275                info.name
276            );
277        }
278    }
279
280    #[test]
281    fn test_node20_template_structure() {
282        let template = Runtime::Node20.template();
283        let dockerfile = Dockerfile::parse(template).expect("Should parse");
284
285        // Should be multi-stage
286        assert_eq!(dockerfile.stages.len(), 2);
287
288        // First stage is builder
289        assert_eq!(dockerfile.stages[0].name, Some("builder".to_string()));
290
291        // Final stage should have USER instruction for security
292        let final_stage = dockerfile.final_stage().unwrap();
293        let has_user = final_stage
294            .instructions
295            .iter()
296            .any(|i| matches!(i, crate::Instruction::User(_)));
297        assert!(has_user, "Node template should run as non-root user");
298    }
299
300    #[test]
301    fn test_rust_template_structure() {
302        let template = Runtime::Rust.template();
303        let dockerfile = Dockerfile::parse(template).expect("Should parse");
304
305        // Should be multi-stage
306        assert_eq!(dockerfile.stages.len(), 2);
307
308        // First stage is builder
309        assert_eq!(dockerfile.stages[0].name, Some("builder".to_string()));
310    }
311
312    #[test]
313    fn test_list_templates() {
314        let templates = list_templates();
315        assert!(!templates.is_empty());
316        assert!(templates.iter().any(|t| t.name == "node20"));
317        assert!(templates.iter().any(|t| t.name == "rust"));
318        assert!(templates.iter().any(|t| t.name == "go"));
319    }
320
321    #[test]
322    fn test_get_template_by_name() {
323        let template = get_template_by_name("node20");
324        assert!(template.is_some());
325        assert!(template.unwrap().contains("node:20"));
326
327        let template = get_template_by_name("unknown");
328        assert!(template.is_none());
329    }
330
331    #[test]
332    fn test_resolve_runtime_explicit() {
333        let dir = TempDir::new().unwrap();
334
335        // Explicit name takes precedence
336        let runtime = resolve_runtime(Some("rust"), dir.path(), false);
337        assert_eq!(runtime, Some(Runtime::Rust));
338    }
339
340    #[test]
341    fn test_resolve_runtime_detect() {
342        let dir = TempDir::new().unwrap();
343        fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
344
345        // Auto-detect when no name given
346        let runtime = resolve_runtime(None, dir.path(), false);
347        assert_eq!(runtime, Some(Runtime::Rust));
348    }
349
350    #[test]
351    fn test_runtime_display() {
352        assert_eq!(format!("{}", Runtime::Node20), "node20");
353        assert_eq!(format!("{}", Runtime::Rust), "rust");
354    }
355
356    #[test]
357    fn test_runtime_from_str() {
358        let runtime: Result<Runtime, _> = "node20".parse();
359        assert_eq!(runtime, Ok(Runtime::Node20));
360
361        let runtime: Result<Runtime, _> = "unknown".parse();
362        assert!(runtime.is_err());
363    }
364}