mod detect;
use std::fmt;
use std::path::Path;
use std::str::FromStr;
pub use detect::{detect_runtime, detect_runtime_with_version};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Runtime {
Node20,
Node22,
Python312,
Python313,
Rust,
Go,
Deno,
Bun,
Wasm(WasmTargetHint),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum WasmTargetHint {
Module,
Component,
#[default]
Auto,
}
impl WasmTargetHint {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Module => "module",
Self::Component => "component",
Self::Auto => "auto",
}
}
}
impl Runtime {
#[must_use]
pub fn all() -> &'static [RuntimeInfo] {
&[
RuntimeInfo {
runtime: Runtime::Node20,
name: "node20",
description: "Node.js 20 (LTS) - Alpine-based, production optimized",
detect_files: &["package.json"],
},
RuntimeInfo {
runtime: Runtime::Node22,
name: "node22",
description: "Node.js 22 (Current) - Alpine-based, production optimized",
detect_files: &["package.json"],
},
RuntimeInfo {
runtime: Runtime::Python312,
name: "python312",
description: "Python 3.12 - Slim Debian-based with pip",
detect_files: &["requirements.txt", "pyproject.toml", "setup.py"],
},
RuntimeInfo {
runtime: Runtime::Python313,
name: "python313",
description: "Python 3.13 - Slim Debian-based with pip",
detect_files: &["requirements.txt", "pyproject.toml", "setup.py"],
},
RuntimeInfo {
runtime: Runtime::Rust,
name: "rust",
description: "Rust - Static musl binary, minimal Alpine runtime",
detect_files: &["Cargo.toml"],
},
RuntimeInfo {
runtime: Runtime::Go,
name: "go",
description: "Go - Static binary, minimal Alpine runtime",
detect_files: &["go.mod"],
},
RuntimeInfo {
runtime: Runtime::Deno,
name: "deno",
description: "Deno - Official runtime with TypeScript support",
detect_files: &["deno.json", "deno.jsonc"],
},
RuntimeInfo {
runtime: Runtime::Bun,
name: "bun",
description: "Bun - Fast JavaScript runtime and bundler",
detect_files: &["bun.lockb"],
},
RuntimeInfo {
runtime: Runtime::Wasm(WasmTargetHint::Auto),
name: "wasm",
description: "WebAssembly - Delegates to wasm: build mode (auto-detects target)",
detect_files: &["cargo-component.toml", "componentize-py.config"],
},
]
}
#[must_use]
pub fn from_name(name: &str) -> Option<Runtime> {
let name_lower = name.to_lowercase();
match name_lower.as_str() {
"node20" | "node-20" | "nodejs20" | "node" => Some(Runtime::Node20),
"node22" | "node-22" | "nodejs22" => Some(Runtime::Node22),
"python312" | "python-312" | "python3.12" | "python" => Some(Runtime::Python312),
"python313" | "python-313" | "python3.13" => Some(Runtime::Python313),
"rust" | "rs" => Some(Runtime::Rust),
"go" | "golang" => Some(Runtime::Go),
"deno" => Some(Runtime::Deno),
"bun" => Some(Runtime::Bun),
"wasm" | "webassembly" => Some(Runtime::Wasm(WasmTargetHint::Auto)),
"wasm-module" | "wasm-preview1" | "wasm-preview2" => {
Some(Runtime::Wasm(WasmTargetHint::Module))
}
"wasm-component" | "wasi-component" => Some(Runtime::Wasm(WasmTargetHint::Component)),
_ => None,
}
}
#[must_use]
pub fn info(&self) -> &'static RuntimeInfo {
let lookup = match self {
Runtime::Wasm(_) => Runtime::Wasm(WasmTargetHint::Auto),
other => *other,
};
Runtime::all()
.iter()
.find(|info| info.runtime == lookup)
.expect("All runtimes must have info")
}
#[must_use]
pub fn template(&self) -> &'static str {
match self {
Runtime::Node20 => include_str!("dockerfiles/node20.Dockerfile"),
Runtime::Node22 => include_str!("dockerfiles/node22.Dockerfile"),
Runtime::Python312 => include_str!("dockerfiles/python312.Dockerfile"),
Runtime::Python313 => include_str!("dockerfiles/python313.Dockerfile"),
Runtime::Rust => include_str!("dockerfiles/rust.Dockerfile"),
Runtime::Go => include_str!("dockerfiles/go.Dockerfile"),
Runtime::Deno => include_str!("dockerfiles/deno.Dockerfile"),
Runtime::Bun => include_str!("dockerfiles/bun.Dockerfile"),
Runtime::Wasm(hint) => Self::wasm_zimagefile(*hint),
}
}
fn wasm_zimagefile(hint: WasmTargetHint) -> &'static str {
match hint {
WasmTargetHint::Component => "wasm:\n target: preview2\n",
WasmTargetHint::Module => "wasm:\n target: preview1\n",
WasmTargetHint::Auto => "wasm: {}\n",
}
}
#[must_use]
pub fn name(&self) -> &'static str {
self.info().name
}
}
impl fmt::Display for Runtime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl FromStr for Runtime {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Runtime::from_name(s).ok_or_else(|| format!("Unknown runtime: {s}"))
}
}
#[derive(Debug, Clone, Copy)]
pub struct RuntimeInfo {
pub runtime: Runtime,
pub name: &'static str,
pub description: &'static str,
pub detect_files: &'static [&'static str],
}
#[must_use]
pub fn list_templates() -> Vec<&'static RuntimeInfo> {
Runtime::all().iter().collect()
}
#[must_use]
pub fn get_template(runtime: Runtime) -> &'static str {
runtime.template()
}
#[must_use]
pub fn get_template_by_name(name: &str) -> Option<&'static str> {
Runtime::from_name(name).map(|r| r.template())
}
pub fn resolve_runtime(
runtime_name: Option<&str>,
context_path: impl AsRef<Path>,
use_version_hints: bool,
) -> Option<Runtime> {
if let Some(name) = runtime_name {
return Runtime::from_name(name);
}
if use_version_hints {
detect_runtime_with_version(context_path)
} else {
detect_runtime(context_path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Dockerfile;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_runtime_from_name() {
assert_eq!(Runtime::from_name("node20"), Some(Runtime::Node20));
assert_eq!(Runtime::from_name("Node20"), Some(Runtime::Node20));
assert_eq!(Runtime::from_name("node"), Some(Runtime::Node20));
assert_eq!(Runtime::from_name("python"), Some(Runtime::Python312));
assert_eq!(Runtime::from_name("rust"), Some(Runtime::Rust));
assert_eq!(Runtime::from_name("go"), Some(Runtime::Go));
assert_eq!(Runtime::from_name("golang"), Some(Runtime::Go));
assert_eq!(Runtime::from_name("deno"), Some(Runtime::Deno));
assert_eq!(Runtime::from_name("bun"), Some(Runtime::Bun));
assert_eq!(Runtime::from_name("unknown"), None);
}
#[test]
fn test_runtime_info() {
let info = Runtime::Node20.info();
assert_eq!(info.name, "node20");
assert!(info.description.contains("Node.js"));
assert!(info.detect_files.contains(&"package.json"));
}
#[test]
fn test_all_templates_parse_correctly() {
for info in Runtime::all() {
if matches!(info.runtime, Runtime::Wasm(_)) {
continue;
}
let template = info.runtime.template();
let result = Dockerfile::parse(template);
assert!(
result.is_ok(),
"Template {} failed to parse: {:?}",
info.name,
result.err()
);
let dockerfile = result.unwrap();
assert!(
!dockerfile.stages.is_empty(),
"Template {} has no stages",
info.name
);
}
}
#[test]
fn test_runtime_wasm_from_name() {
assert_eq!(
Runtime::from_name("wasm"),
Some(Runtime::Wasm(WasmTargetHint::Auto))
);
assert_eq!(
Runtime::from_name("WASM"),
Some(Runtime::Wasm(WasmTargetHint::Auto))
);
assert_eq!(
Runtime::from_name("webassembly"),
Some(Runtime::Wasm(WasmTargetHint::Auto))
);
assert_eq!(
Runtime::from_name("wasm-component"),
Some(Runtime::Wasm(WasmTargetHint::Component))
);
assert_eq!(
Runtime::from_name("wasm-module"),
Some(Runtime::Wasm(WasmTargetHint::Module))
);
}
#[test]
fn test_runtime_wasm_template_is_zimagefile_yaml() {
let t = Runtime::Wasm(WasmTargetHint::Auto).template();
assert!(t.contains("wasm:"), "template should set wasm mode: {t}");
let component = Runtime::Wasm(WasmTargetHint::Component).template();
assert!(component.contains("preview2"), "component → preview2");
let module = Runtime::Wasm(WasmTargetHint::Module).template();
assert!(module.contains("preview1"), "module → preview1");
}
#[test]
fn test_runtime_wasm_info_lookup() {
let auto = Runtime::Wasm(WasmTargetHint::Auto).info();
let module = Runtime::Wasm(WasmTargetHint::Module).info();
let component = Runtime::Wasm(WasmTargetHint::Component).info();
assert_eq!(auto.name, "wasm");
assert_eq!(module.name, "wasm");
assert_eq!(component.name, "wasm");
}
#[test]
fn test_node20_template_structure() {
let template = Runtime::Node20.template();
let dockerfile = Dockerfile::parse(template).expect("Should parse");
assert_eq!(dockerfile.stages.len(), 2);
assert_eq!(dockerfile.stages[0].name, Some("builder".to_string()));
let final_stage = dockerfile.final_stage().unwrap();
let has_user = final_stage
.instructions
.iter()
.any(|i| matches!(i, crate::Instruction::User(_)));
assert!(has_user, "Node template should run as non-root user");
}
#[test]
fn test_rust_template_structure() {
let template = Runtime::Rust.template();
let dockerfile = Dockerfile::parse(template).expect("Should parse");
assert_eq!(dockerfile.stages.len(), 2);
assert_eq!(dockerfile.stages[0].name, Some("builder".to_string()));
}
#[test]
fn test_list_templates() {
let templates = list_templates();
assert!(!templates.is_empty());
assert!(templates.iter().any(|t| t.name == "node20"));
assert!(templates.iter().any(|t| t.name == "rust"));
assert!(templates.iter().any(|t| t.name == "go"));
}
#[test]
fn test_get_template_by_name() {
let template = get_template_by_name("node20");
assert!(template.is_some());
assert!(template.unwrap().contains("node:20"));
let template = get_template_by_name("unknown");
assert!(template.is_none());
}
#[test]
fn test_resolve_runtime_explicit() {
let dir = TempDir::new().unwrap();
let runtime = resolve_runtime(Some("rust"), dir.path(), false);
assert_eq!(runtime, Some(Runtime::Rust));
}
#[test]
fn test_resolve_runtime_detect() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
let runtime = resolve_runtime(None, dir.path(), false);
assert_eq!(runtime, Some(Runtime::Rust));
}
#[test]
fn test_runtime_display() {
assert_eq!(format!("{}", Runtime::Node20), "node20");
assert_eq!(format!("{}", Runtime::Rust), "rust");
}
#[test]
fn test_runtime_from_str() {
let runtime: Result<Runtime, _> = "node20".parse();
assert_eq!(runtime, Ok(Runtime::Node20));
let runtime: Result<Runtime, _> = "unknown".parse();
assert!(runtime.is_err());
}
}