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 #[must_use]
73 pub fn all() -> &'static [RuntimeInfo] {
74 &[
75 RuntimeInfo {
76 runtime: Runtime::Node20,
77 name: "node20",
78 description: "Node.js 20 (LTS) - Alpine-based, production optimized",
79 detect_files: &["package.json"],
80 },
81 RuntimeInfo {
82 runtime: Runtime::Node22,
83 name: "node22",
84 description: "Node.js 22 (Current) - Alpine-based, production optimized",
85 detect_files: &["package.json"],
86 },
87 RuntimeInfo {
88 runtime: Runtime::Python312,
89 name: "python312",
90 description: "Python 3.12 - Slim Debian-based with pip",
91 detect_files: &["requirements.txt", "pyproject.toml", "setup.py"],
92 },
93 RuntimeInfo {
94 runtime: Runtime::Python313,
95 name: "python313",
96 description: "Python 3.13 - Slim Debian-based with pip",
97 detect_files: &["requirements.txt", "pyproject.toml", "setup.py"],
98 },
99 RuntimeInfo {
100 runtime: Runtime::Rust,
101 name: "rust",
102 description: "Rust - Static musl binary, minimal Alpine runtime",
103 detect_files: &["Cargo.toml"],
104 },
105 RuntimeInfo {
106 runtime: Runtime::Go,
107 name: "go",
108 description: "Go - Static binary, minimal Alpine runtime",
109 detect_files: &["go.mod"],
110 },
111 RuntimeInfo {
112 runtime: Runtime::Deno,
113 name: "deno",
114 description: "Deno - Official runtime with TypeScript support",
115 detect_files: &["deno.json", "deno.jsonc"],
116 },
117 RuntimeInfo {
118 runtime: Runtime::Bun,
119 name: "bun",
120 description: "Bun - Fast JavaScript runtime and bundler",
121 detect_files: &["bun.lockb"],
122 },
123 ]
124 }
125
126 #[must_use]
128 pub fn from_name(name: &str) -> Option<Runtime> {
129 let name_lower = name.to_lowercase();
130 match name_lower.as_str() {
131 "node20" | "node-20" | "nodejs20" | "node" => Some(Runtime::Node20),
132 "node22" | "node-22" | "nodejs22" => Some(Runtime::Node22),
133 "python312" | "python-312" | "python3.12" | "python" => Some(Runtime::Python312),
134 "python313" | "python-313" | "python3.13" => Some(Runtime::Python313),
135 "rust" | "rs" => Some(Runtime::Rust),
136 "go" | "golang" => Some(Runtime::Go),
137 "deno" => Some(Runtime::Deno),
138 "bun" => Some(Runtime::Bun),
139 _ => None,
140 }
141 }
142
143 #[must_use]
149 pub fn info(&self) -> &'static RuntimeInfo {
150 Runtime::all()
151 .iter()
152 .find(|info| info.runtime == *self)
153 .expect("All runtimes must have info")
154 }
155
156 #[must_use]
158 pub fn template(&self) -> &'static str {
159 match self {
160 Runtime::Node20 => include_str!("dockerfiles/node20.Dockerfile"),
161 Runtime::Node22 => include_str!("dockerfiles/node22.Dockerfile"),
162 Runtime::Python312 => include_str!("dockerfiles/python312.Dockerfile"),
163 Runtime::Python313 => include_str!("dockerfiles/python313.Dockerfile"),
164 Runtime::Rust => include_str!("dockerfiles/rust.Dockerfile"),
165 Runtime::Go => include_str!("dockerfiles/go.Dockerfile"),
166 Runtime::Deno => include_str!("dockerfiles/deno.Dockerfile"),
167 Runtime::Bun => include_str!("dockerfiles/bun.Dockerfile"),
168 }
169 }
170
171 #[must_use]
173 pub fn name(&self) -> &'static str {
174 self.info().name
175 }
176}
177
178impl fmt::Display for Runtime {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 write!(f, "{}", self.name())
181 }
182}
183
184impl FromStr for Runtime {
185 type Err = String;
186
187 fn from_str(s: &str) -> Result<Self, Self::Err> {
188 Runtime::from_name(s).ok_or_else(|| format!("Unknown runtime: {s}"))
189 }
190}
191
192#[derive(Debug, Clone, Copy)]
194pub struct RuntimeInfo {
195 pub runtime: Runtime,
197 pub name: &'static str,
199 pub description: &'static str,
201 pub detect_files: &'static [&'static str],
203}
204
205#[must_use]
207pub fn list_templates() -> Vec<&'static RuntimeInfo> {
208 Runtime::all().iter().collect()
209}
210
211#[must_use]
213pub fn get_template(runtime: Runtime) -> &'static str {
214 runtime.template()
215}
216
217#[must_use]
219pub fn get_template_by_name(name: &str) -> Option<&'static str> {
220 Runtime::from_name(name).map(|r| r.template())
221}
222
223pub fn resolve_runtime(
225 runtime_name: Option<&str>,
226 context_path: impl AsRef<Path>,
227 use_version_hints: bool,
228) -> Option<Runtime> {
229 if let Some(name) = runtime_name {
231 return Runtime::from_name(name);
232 }
233
234 if use_version_hints {
236 detect_runtime_with_version(context_path)
237 } else {
238 detect_runtime(context_path)
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::Dockerfile;
246 use std::fs;
247 use tempfile::TempDir;
248
249 #[test]
250 fn test_runtime_from_name() {
251 assert_eq!(Runtime::from_name("node20"), Some(Runtime::Node20));
252 assert_eq!(Runtime::from_name("Node20"), Some(Runtime::Node20));
253 assert_eq!(Runtime::from_name("node"), Some(Runtime::Node20));
254 assert_eq!(Runtime::from_name("python"), Some(Runtime::Python312));
255 assert_eq!(Runtime::from_name("rust"), Some(Runtime::Rust));
256 assert_eq!(Runtime::from_name("go"), Some(Runtime::Go));
257 assert_eq!(Runtime::from_name("golang"), Some(Runtime::Go));
258 assert_eq!(Runtime::from_name("deno"), Some(Runtime::Deno));
259 assert_eq!(Runtime::from_name("bun"), Some(Runtime::Bun));
260 assert_eq!(Runtime::from_name("unknown"), None);
261 }
262
263 #[test]
264 fn test_runtime_info() {
265 let info = Runtime::Node20.info();
266 assert_eq!(info.name, "node20");
267 assert!(info.description.contains("Node.js"));
268 assert!(info.detect_files.contains(&"package.json"));
269 }
270
271 #[test]
272 fn test_all_templates_parse_correctly() {
273 for info in Runtime::all() {
274 let template = info.runtime.template();
275 let result = Dockerfile::parse(template);
276 assert!(
277 result.is_ok(),
278 "Template {} failed to parse: {:?}",
279 info.name,
280 result.err()
281 );
282
283 let dockerfile = result.unwrap();
284 assert!(
285 !dockerfile.stages.is_empty(),
286 "Template {} has no stages",
287 info.name
288 );
289 }
290 }
291
292 #[test]
293 fn test_node20_template_structure() {
294 let template = Runtime::Node20.template();
295 let dockerfile = Dockerfile::parse(template).expect("Should parse");
296
297 assert_eq!(dockerfile.stages.len(), 2);
299
300 assert_eq!(dockerfile.stages[0].name, Some("builder".to_string()));
302
303 let final_stage = dockerfile.final_stage().unwrap();
305 let has_user = final_stage
306 .instructions
307 .iter()
308 .any(|i| matches!(i, crate::Instruction::User(_)));
309 assert!(has_user, "Node template should run as non-root user");
310 }
311
312 #[test]
313 fn test_rust_template_structure() {
314 let template = Runtime::Rust.template();
315 let dockerfile = Dockerfile::parse(template).expect("Should parse");
316
317 assert_eq!(dockerfile.stages.len(), 2);
319
320 assert_eq!(dockerfile.stages[0].name, Some("builder".to_string()));
322 }
323
324 #[test]
325 fn test_list_templates() {
326 let templates = list_templates();
327 assert!(!templates.is_empty());
328 assert!(templates.iter().any(|t| t.name == "node20"));
329 assert!(templates.iter().any(|t| t.name == "rust"));
330 assert!(templates.iter().any(|t| t.name == "go"));
331 }
332
333 #[test]
334 fn test_get_template_by_name() {
335 let template = get_template_by_name("node20");
336 assert!(template.is_some());
337 assert!(template.unwrap().contains("node:20"));
338
339 let template = get_template_by_name("unknown");
340 assert!(template.is_none());
341 }
342
343 #[test]
344 fn test_resolve_runtime_explicit() {
345 let dir = TempDir::new().unwrap();
346
347 let runtime = resolve_runtime(Some("rust"), dir.path(), false);
349 assert_eq!(runtime, Some(Runtime::Rust));
350 }
351
352 #[test]
353 fn test_resolve_runtime_detect() {
354 let dir = TempDir::new().unwrap();
355 fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
356
357 let runtime = resolve_runtime(None, dir.path(), false);
359 assert_eq!(runtime, Some(Runtime::Rust));
360 }
361
362 #[test]
363 fn test_runtime_display() {
364 assert_eq!(format!("{}", Runtime::Node20), "node20");
365 assert_eq!(format!("{}", Runtime::Rust), "rust");
366 }
367
368 #[test]
369 fn test_runtime_from_str() {
370 let runtime: Result<Runtime, _> = "node20".parse();
371 assert_eq!(runtime, Ok(Runtime::Node20));
372
373 let runtime: Result<Runtime, _> = "unknown".parse();
374 assert!(runtime.is_err());
375 }
376}