use std::path::Path;
use super::{Runtime, WasmTargetHint};
pub fn detect_runtime(context_path: impl AsRef<Path>) -> Option<Runtime> {
let path = context_path.as_ref();
if let Some(hint) = detect_wasm_hint(path) {
return Some(Runtime::Wasm(hint));
}
if has_dotnet_project_files(path) {
return Some(Runtime::WindowsServerCore);
}
if path.join("package.json").exists() {
if path.join("bun.lockb").exists() {
return Some(Runtime::Bun);
}
if path.join("deno.json").exists() || path.join("deno.jsonc").exists() {
return Some(Runtime::Deno);
}
return Some(Runtime::Node20);
}
if path.join("deno.json").exists()
|| path.join("deno.jsonc").exists()
|| path.join("deno.lock").exists()
{
return Some(Runtime::Deno);
}
if path.join("Cargo.toml").exists() {
return Some(Runtime::Rust);
}
if path.join("pyproject.toml").exists()
|| path.join("requirements.txt").exists()
|| path.join("setup.py").exists()
|| path.join("Pipfile").exists()
|| path.join("poetry.lock").exists()
{
return Some(Runtime::Python312);
}
if path.join("go.mod").exists() {
return Some(Runtime::Go);
}
if has_windows_exe(path) {
return Some(Runtime::WindowsNanoserver);
}
None
}
fn has_dotnet_project_files(path: &Path) -> bool {
if path.join("project.json").exists() {
return true;
}
dir_has_extension(path, &["sln", "csproj", "vcxproj"])
}
fn has_windows_exe(path: &Path) -> bool {
dir_has_extension(path, &["exe"])
}
fn dir_has_extension(path: &Path, extensions: &[&str]) -> bool {
let Ok(entries) = std::fs::read_dir(path) else {
return false;
};
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
if !file_type.is_file() {
continue;
}
let file_name = entry.file_name();
let name = file_name.to_string_lossy();
if let Some((_, ext)) = name.rsplit_once('.') {
let ext_lower = ext.to_ascii_lowercase();
if extensions
.iter()
.any(|&e| e.eq_ignore_ascii_case(&ext_lower))
{
return true;
}
}
}
false
}
pub fn detect_runtime_with_version(context_path: impl AsRef<Path>) -> Option<Runtime> {
let path = context_path.as_ref();
let base_runtime = detect_runtime(path)?;
match base_runtime {
Runtime::Node20 | Runtime::Node22 => {
if let Some(version) = read_node_version(path) {
if version.starts_with("22") || version.starts_with("v22") {
return Some(Runtime::Node22);
}
if version.starts_with("20") || version.starts_with("v20") {
return Some(Runtime::Node20);
}
}
if let Some(version) = read_package_node_version(path) {
if version.contains("22") {
return Some(Runtime::Node22);
}
}
Some(Runtime::Node20)
}
Runtime::Python312 | Runtime::Python313 => {
if let Some(version) = read_python_version(path) {
if version.starts_with("3.13") {
return Some(Runtime::Python313);
}
}
Some(Runtime::Python312)
}
other => Some(other),
}
}
fn detect_wasm_hint(path: &Path) -> Option<WasmTargetHint> {
if path.join("cargo-component.toml").exists() {
return Some(WasmTargetHint::Component);
}
let cargo_toml = path.join("Cargo.toml");
let cargo_has_component_metadata = if cargo_toml.exists() {
cargo_toml_has_component_metadata(&cargo_toml)
} else {
false
};
if cargo_has_component_metadata {
return Some(WasmTargetHint::Component);
}
if path.join("componentize-py.config").exists() {
return Some(WasmTargetHint::Component);
}
if path.join("package.json").exists() && package_json_uses_jco(&path.join("package.json")) {
return Some(WasmTargetHint::Component);
}
if cargo_toml.exists() && cargo_config_targets_wasip(path) {
return Some(WasmTargetHint::Module);
}
None
}
fn cargo_toml_has_component_metadata(cargo_toml: &Path) -> bool {
let Ok(content) = std::fs::read_to_string(cargo_toml) else {
return false;
};
content.lines().any(|line| {
let trimmed = line.trim();
trimmed == "[package.metadata.component]"
|| trimmed.starts_with("[package.metadata.component.")
})
}
const JCO_DEP_SECTIONS: &[&str] = &[
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
];
fn package_json_uses_jco(package_json: &Path) -> bool {
let Ok(content) = std::fs::read_to_string(package_json) else {
return false;
};
let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
return false;
};
for section in JCO_DEP_SECTIONS {
if let Some(obj) = json.get(section).and_then(|v| v.as_object()) {
if obj.contains_key("jco") || obj.contains_key("@bytecodealliance/jco") {
return true;
}
}
}
false
}
fn cargo_config_targets_wasip(path: &Path) -> bool {
let config = path.join(".cargo").join("config.toml");
let Ok(content) = std::fs::read_to_string(&config) else {
return false;
};
content.lines().any(|line| {
let trimmed = line.trim();
trimmed.contains("wasm32-wasip1") || trimmed.contains("wasm32-wasip2")
})
}
fn read_node_version(path: &Path) -> Option<String> {
for filename in &[".nvmrc", ".node-version"] {
let version_file = path.join(filename);
if version_file.exists() {
if let Ok(content) = std::fs::read_to_string(&version_file) {
let version = content.trim().to_string();
if !version.is_empty() {
return Some(version);
}
}
}
}
None
}
fn read_package_node_version(path: &Path) -> Option<String> {
let package_json = path.join("package.json");
if package_json.exists() {
if let Ok(content) = std::fs::read_to_string(&package_json) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(engines) = json.get("engines") {
if let Some(node) = engines.get("node") {
if let Some(version) = node.as_str() {
return Some(version.to_string());
}
}
}
}
}
}
None
}
fn read_python_version(path: &Path) -> Option<String> {
let python_version = path.join(".python-version");
if python_version.exists() {
if let Ok(content) = std::fs::read_to_string(&python_version) {
let version = content.trim().to_string();
if !version.is_empty() {
return Some(version);
}
}
}
let pyproject = path.join("pyproject.toml");
if pyproject.exists() {
if let Ok(content) = std::fs::read_to_string(&pyproject) {
for line in content.lines() {
let line = line.trim();
if line.starts_with("requires-python") {
if let Some(version) = line.split('=').nth(1) {
let version = version.trim().trim_matches('"').trim_matches('\'');
return Some(version.to_string());
}
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_temp_dir() -> TempDir {
TempDir::new().expect("Failed to create temp directory")
}
#[test]
fn test_detect_nodejs_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("package.json"), "{}").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Node20));
}
#[test]
fn test_detect_bun_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("package.json"), "{}").unwrap();
fs::write(dir.path().join("bun.lockb"), "").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Bun));
}
#[test]
fn test_detect_deno_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("deno.json"), "{}").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Deno));
}
#[test]
fn test_detect_deno_with_package_json() {
let dir = create_temp_dir();
fs::write(dir.path().join("package.json"), "{}").unwrap();
fs::write(dir.path().join("deno.json"), "{}").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Deno));
}
#[test]
fn test_detect_rust_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Rust));
}
#[test]
fn test_detect_python_requirements() {
let dir = create_temp_dir();
fs::write(dir.path().join("requirements.txt"), "flask==2.0").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Python312));
}
#[test]
fn test_detect_python_pyproject() {
let dir = create_temp_dir();
fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Python312));
}
#[test]
fn test_detect_go_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("go.mod"), "module example.com/app").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Go));
}
#[test]
fn test_detect_no_runtime() {
let dir = create_temp_dir();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, None);
}
#[test]
fn test_detect_node22_from_nvmrc() {
let dir = create_temp_dir();
fs::write(dir.path().join("package.json"), "{}").unwrap();
fs::write(dir.path().join(".nvmrc"), "22.0.0").unwrap();
let runtime = detect_runtime_with_version(dir.path());
assert_eq!(runtime, Some(Runtime::Node22));
}
#[test]
fn test_detect_node22_from_package_engines() {
let dir = create_temp_dir();
let package_json = r#"{"engines": {"node": ">=22.0.0"}}"#;
fs::write(dir.path().join("package.json"), package_json).unwrap();
let runtime = detect_runtime_with_version(dir.path());
assert_eq!(runtime, Some(Runtime::Node22));
}
#[test]
fn test_detect_python313_from_version_file() {
let dir = create_temp_dir();
fs::write(dir.path().join("requirements.txt"), "flask").unwrap();
fs::write(dir.path().join(".python-version"), "3.13.0").unwrap();
let runtime = detect_runtime_with_version(dir.path());
assert_eq!(runtime, Some(Runtime::Python313));
}
#[test]
fn test_detect_wasm_cargo_component_toml() {
let dir = create_temp_dir();
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
fs::write(dir.path().join("cargo-component.toml"), "").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
}
#[test]
fn test_detect_wasm_cargo_metadata_component() {
let dir = create_temp_dir();
let toml = r#"
[package]
name = "foo"
version = "0.1.0"
[package.metadata.component]
package = "zlayer:example"
"#;
fs::write(dir.path().join("Cargo.toml"), toml).unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
}
#[test]
fn test_detect_wasm_componentize_py() {
let dir = create_temp_dir();
fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
fs::write(dir.path().join("componentize-py.config"), "").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
}
#[test]
fn test_detect_wasm_jco_in_package_json() {
let dir = create_temp_dir();
let pkg = r#"{
"name": "foo",
"devDependencies": { "@bytecodealliance/jco": "^1.0.0" }
}"#;
fs::write(dir.path().join("package.json"), pkg).unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
}
#[test]
fn test_detect_wasm_cargo_config_wasip1_module() {
let dir = create_temp_dir();
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
fs::create_dir_all(dir.path().join(".cargo")).unwrap();
fs::write(
dir.path().join(".cargo").join("config.toml"),
"[build]\ntarget = \"wasm32-wasip1\"\n",
)
.unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Module)));
}
#[test]
fn test_plain_rust_project_is_not_wasm() {
let dir = create_temp_dir();
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Rust));
}
#[test]
fn test_plain_node_project_is_not_wasm() {
let dir = create_temp_dir();
fs::write(
dir.path().join("package.json"),
r#"{"dependencies":{"express":"^4"}}"#,
)
.unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Node20));
}
#[test]
fn test_detect_windows_servercore_from_sln() {
let dir = create_temp_dir();
fs::write(dir.path().join("MyApp.sln"), "").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::WindowsServerCore));
}
#[test]
fn test_detect_windows_servercore_from_csproj() {
let dir = create_temp_dir();
fs::write(
dir.path().join("MyApp.csproj"),
r#"<Project Sdk="Microsoft.NET.Sdk" />"#,
)
.unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::WindowsServerCore));
}
#[test]
fn test_detect_windows_servercore_from_vcxproj() {
let dir = create_temp_dir();
fs::write(dir.path().join("Native.vcxproj"), "").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::WindowsServerCore));
}
#[test]
fn test_detect_windows_servercore_from_legacy_project_json() {
let dir = create_temp_dir();
fs::write(dir.path().join("project.json"), "{}").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::WindowsServerCore));
}
#[test]
fn test_detect_windows_nanoserver_from_standalone_exe() {
let dir = create_temp_dir();
fs::write(dir.path().join("app.exe"), b"MZ\x90\x00").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::WindowsNanoserver));
}
#[test]
fn test_detect_case_insensitive_exe_extension() {
let dir = create_temp_dir();
fs::write(dir.path().join("App.EXE"), b"MZ").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::WindowsNanoserver));
}
#[test]
fn test_dotnet_wins_over_linux_python_hint() {
let dir = create_temp_dir();
fs::write(dir.path().join("MyApp.sln"), "").unwrap();
fs::write(dir.path().join("requirements.txt"), "requests").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::WindowsServerCore));
}
#[test]
fn test_exe_does_not_override_rust_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
fs::write(dir.path().join("helper.exe"), b"MZ").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Rust));
}
#[test]
fn test_exe_does_not_override_go_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("go.mod"), "module example.com/app").unwrap();
fs::write(dir.path().join("helper.exe"), b"MZ").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Go));
}
#[test]
fn test_exe_does_not_override_node_project() {
let dir = create_temp_dir();
fs::write(dir.path().join("package.json"), "{}").unwrap();
fs::write(dir.path().join("helper.exe"), b"MZ").unwrap();
let runtime = detect_runtime(dir.path());
assert_eq!(runtime, Some(Runtime::Node20));
}
}