1mod 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 Wasm(WasmTargetHint),
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
83pub enum WasmTargetHint {
84 Module,
86 Component,
88 #[default]
90 Auto,
91}
92
93impl WasmTargetHint {
94 #[must_use]
96 pub fn as_str(&self) -> &'static str {
97 match self {
98 Self::Module => "module",
99 Self::Component => "component",
100 Self::Auto => "auto",
101 }
102 }
103}
104
105impl Runtime {
106 #[must_use]
108 pub fn all() -> &'static [RuntimeInfo] {
109 &[
110 RuntimeInfo {
111 runtime: Runtime::Node20,
112 name: "node20",
113 description: "Node.js 20 (LTS) - Alpine-based, production optimized",
114 detect_files: &["package.json"],
115 },
116 RuntimeInfo {
117 runtime: Runtime::Node22,
118 name: "node22",
119 description: "Node.js 22 (Current) - Alpine-based, production optimized",
120 detect_files: &["package.json"],
121 },
122 RuntimeInfo {
123 runtime: Runtime::Python312,
124 name: "python312",
125 description: "Python 3.12 - Slim Debian-based with pip",
126 detect_files: &["requirements.txt", "pyproject.toml", "setup.py"],
127 },
128 RuntimeInfo {
129 runtime: Runtime::Python313,
130 name: "python313",
131 description: "Python 3.13 - Slim Debian-based with pip",
132 detect_files: &["requirements.txt", "pyproject.toml", "setup.py"],
133 },
134 RuntimeInfo {
135 runtime: Runtime::Rust,
136 name: "rust",
137 description: "Rust - Static musl binary, minimal Alpine runtime",
138 detect_files: &["Cargo.toml"],
139 },
140 RuntimeInfo {
141 runtime: Runtime::Go,
142 name: "go",
143 description: "Go - Static binary, minimal Alpine runtime",
144 detect_files: &["go.mod"],
145 },
146 RuntimeInfo {
147 runtime: Runtime::Deno,
148 name: "deno",
149 description: "Deno - Official runtime with TypeScript support",
150 detect_files: &["deno.json", "deno.jsonc"],
151 },
152 RuntimeInfo {
153 runtime: Runtime::Bun,
154 name: "bun",
155 description: "Bun - Fast JavaScript runtime and bundler",
156 detect_files: &["bun.lockb"],
157 },
158 RuntimeInfo {
159 runtime: Runtime::Wasm(WasmTargetHint::Auto),
160 name: "wasm",
161 description: "WebAssembly - Delegates to wasm: build mode (auto-detects target)",
162 detect_files: &["cargo-component.toml", "componentize-py.config"],
163 },
164 ]
165 }
166
167 #[must_use]
169 pub fn from_name(name: &str) -> Option<Runtime> {
170 let name_lower = name.to_lowercase();
171 match name_lower.as_str() {
172 "node20" | "node-20" | "nodejs20" | "node" => Some(Runtime::Node20),
173 "node22" | "node-22" | "nodejs22" => Some(Runtime::Node22),
174 "python312" | "python-312" | "python3.12" | "python" => Some(Runtime::Python312),
175 "python313" | "python-313" | "python3.13" => Some(Runtime::Python313),
176 "rust" | "rs" => Some(Runtime::Rust),
177 "go" | "golang" => Some(Runtime::Go),
178 "deno" => Some(Runtime::Deno),
179 "bun" => Some(Runtime::Bun),
180 "wasm" | "webassembly" => Some(Runtime::Wasm(WasmTargetHint::Auto)),
181 "wasm-module" | "wasm-preview1" | "wasm-preview2" => {
182 Some(Runtime::Wasm(WasmTargetHint::Module))
183 }
184 "wasm-component" | "wasi-component" => Some(Runtime::Wasm(WasmTargetHint::Component)),
185 _ => None,
186 }
187 }
188
189 #[must_use]
195 pub fn info(&self) -> &'static RuntimeInfo {
196 let lookup = match self {
200 Runtime::Wasm(_) => Runtime::Wasm(WasmTargetHint::Auto),
201 other => *other,
202 };
203 Runtime::all()
204 .iter()
205 .find(|info| info.runtime == lookup)
206 .expect("All runtimes must have info")
207 }
208
209 #[must_use]
220 pub fn template(&self) -> &'static str {
221 match self {
222 Runtime::Node20 => include_str!("dockerfiles/node20.Dockerfile"),
223 Runtime::Node22 => include_str!("dockerfiles/node22.Dockerfile"),
224 Runtime::Python312 => include_str!("dockerfiles/python312.Dockerfile"),
225 Runtime::Python313 => include_str!("dockerfiles/python313.Dockerfile"),
226 Runtime::Rust => include_str!("dockerfiles/rust.Dockerfile"),
227 Runtime::Go => include_str!("dockerfiles/go.Dockerfile"),
228 Runtime::Deno => include_str!("dockerfiles/deno.Dockerfile"),
229 Runtime::Bun => include_str!("dockerfiles/bun.Dockerfile"),
230 Runtime::Wasm(hint) => Self::wasm_zimagefile(*hint),
231 }
232 }
233
234 fn wasm_zimagefile(hint: WasmTargetHint) -> &'static str {
241 match hint {
242 WasmTargetHint::Component => "wasm:\n target: preview2\n",
244 WasmTargetHint::Module => "wasm:\n target: preview1\n",
247 WasmTargetHint::Auto => "wasm: {}\n",
249 }
250 }
251
252 #[must_use]
254 pub fn name(&self) -> &'static str {
255 self.info().name
256 }
257}
258
259impl fmt::Display for Runtime {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 write!(f, "{}", self.name())
262 }
263}
264
265impl FromStr for Runtime {
266 type Err = String;
267
268 fn from_str(s: &str) -> Result<Self, Self::Err> {
269 Runtime::from_name(s).ok_or_else(|| format!("Unknown runtime: {s}"))
270 }
271}
272
273#[derive(Debug, Clone, Copy)]
275pub struct RuntimeInfo {
276 pub runtime: Runtime,
278 pub name: &'static str,
280 pub description: &'static str,
282 pub detect_files: &'static [&'static str],
284}
285
286#[must_use]
288pub fn list_templates() -> Vec<&'static RuntimeInfo> {
289 Runtime::all().iter().collect()
290}
291
292#[must_use]
294pub fn get_template(runtime: Runtime) -> &'static str {
295 runtime.template()
296}
297
298#[must_use]
300pub fn get_template_by_name(name: &str) -> Option<&'static str> {
301 Runtime::from_name(name).map(|r| r.template())
302}
303
304pub fn resolve_runtime(
306 runtime_name: Option<&str>,
307 context_path: impl AsRef<Path>,
308 use_version_hints: bool,
309) -> Option<Runtime> {
310 if let Some(name) = runtime_name {
312 return Runtime::from_name(name);
313 }
314
315 if use_version_hints {
317 detect_runtime_with_version(context_path)
318 } else {
319 detect_runtime(context_path)
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::Dockerfile;
327 use std::fs;
328 use tempfile::TempDir;
329
330 #[test]
331 fn test_runtime_from_name() {
332 assert_eq!(Runtime::from_name("node20"), Some(Runtime::Node20));
333 assert_eq!(Runtime::from_name("Node20"), Some(Runtime::Node20));
334 assert_eq!(Runtime::from_name("node"), Some(Runtime::Node20));
335 assert_eq!(Runtime::from_name("python"), Some(Runtime::Python312));
336 assert_eq!(Runtime::from_name("rust"), Some(Runtime::Rust));
337 assert_eq!(Runtime::from_name("go"), Some(Runtime::Go));
338 assert_eq!(Runtime::from_name("golang"), Some(Runtime::Go));
339 assert_eq!(Runtime::from_name("deno"), Some(Runtime::Deno));
340 assert_eq!(Runtime::from_name("bun"), Some(Runtime::Bun));
341 assert_eq!(Runtime::from_name("unknown"), None);
342 }
343
344 #[test]
345 fn test_runtime_info() {
346 let info = Runtime::Node20.info();
347 assert_eq!(info.name, "node20");
348 assert!(info.description.contains("Node.js"));
349 assert!(info.detect_files.contains(&"package.json"));
350 }
351
352 #[test]
353 fn test_all_templates_parse_correctly() {
354 for info in Runtime::all() {
355 if matches!(info.runtime, Runtime::Wasm(_)) {
359 continue;
360 }
361
362 let template = info.runtime.template();
363 let result = Dockerfile::parse(template);
364 assert!(
365 result.is_ok(),
366 "Template {} failed to parse: {:?}",
367 info.name,
368 result.err()
369 );
370
371 let dockerfile = result.unwrap();
372 assert!(
373 !dockerfile.stages.is_empty(),
374 "Template {} has no stages",
375 info.name
376 );
377 }
378 }
379
380 #[test]
381 fn test_runtime_wasm_from_name() {
382 assert_eq!(
383 Runtime::from_name("wasm"),
384 Some(Runtime::Wasm(WasmTargetHint::Auto))
385 );
386 assert_eq!(
387 Runtime::from_name("WASM"),
388 Some(Runtime::Wasm(WasmTargetHint::Auto))
389 );
390 assert_eq!(
391 Runtime::from_name("webassembly"),
392 Some(Runtime::Wasm(WasmTargetHint::Auto))
393 );
394 assert_eq!(
395 Runtime::from_name("wasm-component"),
396 Some(Runtime::Wasm(WasmTargetHint::Component))
397 );
398 assert_eq!(
399 Runtime::from_name("wasm-module"),
400 Some(Runtime::Wasm(WasmTargetHint::Module))
401 );
402 }
403
404 #[test]
405 fn test_runtime_wasm_template_is_zimagefile_yaml() {
406 let t = Runtime::Wasm(WasmTargetHint::Auto).template();
407 assert!(t.contains("wasm:"), "template should set wasm mode: {t}");
408
409 let component = Runtime::Wasm(WasmTargetHint::Component).template();
410 assert!(component.contains("preview2"), "component → preview2");
411
412 let module = Runtime::Wasm(WasmTargetHint::Module).template();
413 assert!(module.contains("preview1"), "module → preview1");
414 }
415
416 #[test]
417 fn test_runtime_wasm_info_lookup() {
418 let auto = Runtime::Wasm(WasmTargetHint::Auto).info();
421 let module = Runtime::Wasm(WasmTargetHint::Module).info();
422 let component = Runtime::Wasm(WasmTargetHint::Component).info();
423 assert_eq!(auto.name, "wasm");
424 assert_eq!(module.name, "wasm");
425 assert_eq!(component.name, "wasm");
426 }
427
428 #[test]
429 fn test_node20_template_structure() {
430 let template = Runtime::Node20.template();
431 let dockerfile = Dockerfile::parse(template).expect("Should parse");
432
433 assert_eq!(dockerfile.stages.len(), 2);
435
436 assert_eq!(dockerfile.stages[0].name, Some("builder".to_string()));
438
439 let final_stage = dockerfile.final_stage().unwrap();
441 let has_user = final_stage
442 .instructions
443 .iter()
444 .any(|i| matches!(i, crate::Instruction::User(_)));
445 assert!(has_user, "Node template should run as non-root user");
446 }
447
448 #[test]
449 fn test_rust_template_structure() {
450 let template = Runtime::Rust.template();
451 let dockerfile = Dockerfile::parse(template).expect("Should parse");
452
453 assert_eq!(dockerfile.stages.len(), 2);
455
456 assert_eq!(dockerfile.stages[0].name, Some("builder".to_string()));
458 }
459
460 #[test]
461 fn test_list_templates() {
462 let templates = list_templates();
463 assert!(!templates.is_empty());
464 assert!(templates.iter().any(|t| t.name == "node20"));
465 assert!(templates.iter().any(|t| t.name == "rust"));
466 assert!(templates.iter().any(|t| t.name == "go"));
467 }
468
469 #[test]
470 fn test_get_template_by_name() {
471 let template = get_template_by_name("node20");
472 assert!(template.is_some());
473 assert!(template.unwrap().contains("node:20"));
474
475 let template = get_template_by_name("unknown");
476 assert!(template.is_none());
477 }
478
479 #[test]
480 fn test_resolve_runtime_explicit() {
481 let dir = TempDir::new().unwrap();
482
483 let runtime = resolve_runtime(Some("rust"), dir.path(), false);
485 assert_eq!(runtime, Some(Runtime::Rust));
486 }
487
488 #[test]
489 fn test_resolve_runtime_detect() {
490 let dir = TempDir::new().unwrap();
491 fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
492
493 let runtime = resolve_runtime(None, dir.path(), false);
495 assert_eq!(runtime, Some(Runtime::Rust));
496 }
497
498 #[test]
499 fn test_runtime_display() {
500 assert_eq!(format!("{}", Runtime::Node20), "node20");
501 assert_eq!(format!("{}", Runtime::Rust), "rust");
502 }
503
504 #[test]
505 fn test_runtime_from_str() {
506 let runtime: Result<Runtime, _> = "node20".parse();
507 assert_eq!(runtime, Ok(Runtime::Node20));
508
509 let runtime: Result<Runtime, _> = "unknown".parse();
510 assert!(runtime.is_err());
511 }
512}