zlayer_builder/templates/
mod.rs1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum Runtime {
52 Node20,
54 Node22,
56 Python312,
58 Python313,
60 Rust,
62 Go,
64 Deno,
66 Bun,
68}
69
70impl Runtime {
71 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 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 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 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 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#[derive(Debug, Clone, Copy)]
185pub struct RuntimeInfo {
186 pub runtime: Runtime,
188 pub name: &'static str,
190 pub description: &'static str,
192 pub detect_files: &'static [&'static str],
194}
195
196pub fn list_templates() -> Vec<&'static RuntimeInfo> {
198 Runtime::all().iter().collect()
199}
200
201pub fn get_template(runtime: Runtime) -> &'static str {
203 runtime.template()
204}
205
206pub fn get_template_by_name(name: &str) -> Option<&'static str> {
208 Runtime::from_name(name).map(|r| r.template())
209}
210
211pub fn resolve_runtime(
213 runtime_name: Option<&str>,
214 context_path: impl AsRef<Path>,
215 use_version_hints: bool,
216) -> Option<Runtime> {
217 if let Some(name) = runtime_name {
219 return Runtime::from_name(name);
220 }
221
222 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 assert_eq!(dockerfile.stages.len(), 2);
287
288 assert_eq!(dockerfile.stages[0].name, Some("builder".to_string()));
290
291 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 assert_eq!(dockerfile.stages.len(), 2);
307
308 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 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 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}