zlayer_builder/templates/
detect.rs1use std::path::Path;
7
8use super::{Runtime, WasmTargetHint};
9
10pub fn detect_runtime(context_path: impl AsRef<Path>) -> Option<Runtime> {
27 let path = context_path.as_ref();
28
29 if let Some(hint) = detect_wasm_hint(path) {
34 return Some(Runtime::Wasm(hint));
35 }
36
37 if path.join("package.json").exists() {
39 if path.join("bun.lockb").exists() {
41 return Some(Runtime::Bun);
42 }
43
44 if path.join("deno.json").exists() || path.join("deno.jsonc").exists() {
46 return Some(Runtime::Deno);
47 }
48
49 return Some(Runtime::Node20);
51 }
52
53 if path.join("deno.json").exists()
55 || path.join("deno.jsonc").exists()
56 || path.join("deno.lock").exists()
57 {
58 return Some(Runtime::Deno);
59 }
60
61 if path.join("Cargo.toml").exists() {
63 return Some(Runtime::Rust);
64 }
65
66 if path.join("pyproject.toml").exists()
68 || path.join("requirements.txt").exists()
69 || path.join("setup.py").exists()
70 || path.join("Pipfile").exists()
71 || path.join("poetry.lock").exists()
72 {
73 return Some(Runtime::Python312);
74 }
75
76 if path.join("go.mod").exists() {
78 return Some(Runtime::Go);
79 }
80
81 None
82}
83
84pub fn detect_runtime_with_version(context_path: impl AsRef<Path>) -> Option<Runtime> {
89 let path = context_path.as_ref();
90
91 let base_runtime = detect_runtime(path)?;
93
94 match base_runtime {
96 Runtime::Node20 | Runtime::Node22 => {
97 if let Some(version) = read_node_version(path) {
99 if version.starts_with("22") || version.starts_with("v22") {
100 return Some(Runtime::Node22);
101 }
102 if version.starts_with("20") || version.starts_with("v20") {
103 return Some(Runtime::Node20);
104 }
105 }
106
107 if let Some(version) = read_package_node_version(path) {
109 if version.contains("22") {
110 return Some(Runtime::Node22);
111 }
112 }
113
114 Some(Runtime::Node20)
115 }
116 Runtime::Python312 | Runtime::Python313 => {
117 if let Some(version) = read_python_version(path) {
119 if version.starts_with("3.13") {
120 return Some(Runtime::Python313);
121 }
122 }
123
124 Some(Runtime::Python312)
125 }
126 other => Some(other),
127 }
128}
129
130fn detect_wasm_hint(path: &Path) -> Option<WasmTargetHint> {
144 if path.join("cargo-component.toml").exists() {
146 return Some(WasmTargetHint::Component);
147 }
148
149 let cargo_toml = path.join("Cargo.toml");
153 let cargo_has_component_metadata = if cargo_toml.exists() {
154 cargo_toml_has_component_metadata(&cargo_toml)
155 } else {
156 false
157 };
158 if cargo_has_component_metadata {
159 return Some(WasmTargetHint::Component);
160 }
161
162 if path.join("componentize-py.config").exists() {
164 return Some(WasmTargetHint::Component);
165 }
166
167 if path.join("package.json").exists() && package_json_uses_jco(&path.join("package.json")) {
169 return Some(WasmTargetHint::Component);
170 }
171
172 if cargo_toml.exists() && cargo_config_targets_wasip(path) {
174 return Some(WasmTargetHint::Module);
175 }
176
177 None
178}
179
180fn cargo_toml_has_component_metadata(cargo_toml: &Path) -> bool {
185 let Ok(content) = std::fs::read_to_string(cargo_toml) else {
186 return false;
187 };
188 content.lines().any(|line| {
189 let trimmed = line.trim();
190 trimmed == "[package.metadata.component]"
191 || trimmed.starts_with("[package.metadata.component.")
192 })
193}
194
195const JCO_DEP_SECTIONS: &[&str] = &[
197 "dependencies",
198 "devDependencies",
199 "peerDependencies",
200 "optionalDependencies",
201];
202
203fn package_json_uses_jco(package_json: &Path) -> bool {
206 let Ok(content) = std::fs::read_to_string(package_json) else {
207 return false;
208 };
209 let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
210 return false;
211 };
212
213 for section in JCO_DEP_SECTIONS {
214 if let Some(obj) = json.get(section).and_then(|v| v.as_object()) {
215 if obj.contains_key("jco") || obj.contains_key("@bytecodealliance/jco") {
216 return true;
217 }
218 }
219 }
220 false
221}
222
223fn cargo_config_targets_wasip(path: &Path) -> bool {
226 let config = path.join(".cargo").join("config.toml");
227 let Ok(content) = std::fs::read_to_string(&config) else {
228 return false;
229 };
230 content.lines().any(|line| {
231 let trimmed = line.trim();
232 trimmed.contains("wasm32-wasip1") || trimmed.contains("wasm32-wasip2")
233 })
234}
235
236fn read_node_version(path: &Path) -> Option<String> {
238 for filename in &[".nvmrc", ".node-version"] {
239 let version_file = path.join(filename);
240 if version_file.exists() {
241 if let Ok(content) = std::fs::read_to_string(&version_file) {
242 let version = content.trim().to_string();
243 if !version.is_empty() {
244 return Some(version);
245 }
246 }
247 }
248 }
249 None
250}
251
252fn read_package_node_version(path: &Path) -> Option<String> {
254 let package_json = path.join("package.json");
255 if package_json.exists() {
256 if let Ok(content) = std::fs::read_to_string(&package_json) {
257 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
258 if let Some(engines) = json.get("engines") {
259 if let Some(node) = engines.get("node") {
260 if let Some(version) = node.as_str() {
261 return Some(version.to_string());
262 }
263 }
264 }
265 }
266 }
267 }
268 None
269}
270
271fn read_python_version(path: &Path) -> Option<String> {
273 let python_version = path.join(".python-version");
275 if python_version.exists() {
276 if let Ok(content) = std::fs::read_to_string(&python_version) {
277 let version = content.trim().to_string();
278 if !version.is_empty() {
279 return Some(version);
280 }
281 }
282 }
283
284 let pyproject = path.join("pyproject.toml");
286 if pyproject.exists() {
287 if let Ok(content) = std::fs::read_to_string(&pyproject) {
288 for line in content.lines() {
290 let line = line.trim();
291 if line.starts_with("requires-python") {
292 if let Some(version) = line.split('=').nth(1) {
293 let version = version.trim().trim_matches('"').trim_matches('\'');
294 return Some(version.to_string());
295 }
296 }
297 }
298 }
299 }
300
301 None
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use std::fs;
308 use tempfile::TempDir;
309
310 fn create_temp_dir() -> TempDir {
311 TempDir::new().expect("Failed to create temp directory")
312 }
313
314 #[test]
315 fn test_detect_nodejs_project() {
316 let dir = create_temp_dir();
317 fs::write(dir.path().join("package.json"), "{}").unwrap();
318
319 let runtime = detect_runtime(dir.path());
320 assert_eq!(runtime, Some(Runtime::Node20));
321 }
322
323 #[test]
324 fn test_detect_bun_project() {
325 let dir = create_temp_dir();
326 fs::write(dir.path().join("package.json"), "{}").unwrap();
327 fs::write(dir.path().join("bun.lockb"), "").unwrap();
328
329 let runtime = detect_runtime(dir.path());
330 assert_eq!(runtime, Some(Runtime::Bun));
331 }
332
333 #[test]
334 fn test_detect_deno_project() {
335 let dir = create_temp_dir();
336 fs::write(dir.path().join("deno.json"), "{}").unwrap();
337
338 let runtime = detect_runtime(dir.path());
339 assert_eq!(runtime, Some(Runtime::Deno));
340 }
341
342 #[test]
343 fn test_detect_deno_with_package_json() {
344 let dir = create_temp_dir();
345 fs::write(dir.path().join("package.json"), "{}").unwrap();
346 fs::write(dir.path().join("deno.json"), "{}").unwrap();
347
348 let runtime = detect_runtime(dir.path());
349 assert_eq!(runtime, Some(Runtime::Deno));
350 }
351
352 #[test]
353 fn test_detect_rust_project() {
354 let dir = create_temp_dir();
355 fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
356
357 let runtime = detect_runtime(dir.path());
358 assert_eq!(runtime, Some(Runtime::Rust));
359 }
360
361 #[test]
362 fn test_detect_python_requirements() {
363 let dir = create_temp_dir();
364 fs::write(dir.path().join("requirements.txt"), "flask==2.0").unwrap();
365
366 let runtime = detect_runtime(dir.path());
367 assert_eq!(runtime, Some(Runtime::Python312));
368 }
369
370 #[test]
371 fn test_detect_python_pyproject() {
372 let dir = create_temp_dir();
373 fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
374
375 let runtime = detect_runtime(dir.path());
376 assert_eq!(runtime, Some(Runtime::Python312));
377 }
378
379 #[test]
380 fn test_detect_go_project() {
381 let dir = create_temp_dir();
382 fs::write(dir.path().join("go.mod"), "module example.com/app").unwrap();
383
384 let runtime = detect_runtime(dir.path());
385 assert_eq!(runtime, Some(Runtime::Go));
386 }
387
388 #[test]
389 fn test_detect_no_runtime() {
390 let dir = create_temp_dir();
391 let runtime = detect_runtime(dir.path());
394 assert_eq!(runtime, None);
395 }
396
397 #[test]
398 fn test_detect_node22_from_nvmrc() {
399 let dir = create_temp_dir();
400 fs::write(dir.path().join("package.json"), "{}").unwrap();
401 fs::write(dir.path().join(".nvmrc"), "22.0.0").unwrap();
402
403 let runtime = detect_runtime_with_version(dir.path());
404 assert_eq!(runtime, Some(Runtime::Node22));
405 }
406
407 #[test]
408 fn test_detect_node22_from_package_engines() {
409 let dir = create_temp_dir();
410 let package_json = r#"{"engines": {"node": ">=22.0.0"}}"#;
411 fs::write(dir.path().join("package.json"), package_json).unwrap();
412
413 let runtime = detect_runtime_with_version(dir.path());
414 assert_eq!(runtime, Some(Runtime::Node22));
415 }
416
417 #[test]
418 fn test_detect_python313_from_version_file() {
419 let dir = create_temp_dir();
420 fs::write(dir.path().join("requirements.txt"), "flask").unwrap();
421 fs::write(dir.path().join(".python-version"), "3.13.0").unwrap();
422
423 let runtime = detect_runtime_with_version(dir.path());
424 assert_eq!(runtime, Some(Runtime::Python313));
425 }
426
427 #[test]
430 fn test_detect_wasm_cargo_component_toml() {
431 let dir = create_temp_dir();
432 fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
433 fs::write(dir.path().join("cargo-component.toml"), "").unwrap();
434
435 let runtime = detect_runtime(dir.path());
436 assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
437 }
438
439 #[test]
440 fn test_detect_wasm_cargo_metadata_component() {
441 let dir = create_temp_dir();
442 let toml = r#"
443[package]
444name = "foo"
445version = "0.1.0"
446
447[package.metadata.component]
448package = "zlayer:example"
449"#;
450 fs::write(dir.path().join("Cargo.toml"), toml).unwrap();
451
452 let runtime = detect_runtime(dir.path());
453 assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
454 }
455
456 #[test]
457 fn test_detect_wasm_componentize_py() {
458 let dir = create_temp_dir();
459 fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
460 fs::write(dir.path().join("componentize-py.config"), "").unwrap();
461
462 let runtime = detect_runtime(dir.path());
463 assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
464 }
465
466 #[test]
467 fn test_detect_wasm_jco_in_package_json() {
468 let dir = create_temp_dir();
469 let pkg = r#"{
470 "name": "foo",
471 "devDependencies": { "@bytecodealliance/jco": "^1.0.0" }
472}"#;
473 fs::write(dir.path().join("package.json"), pkg).unwrap();
474
475 let runtime = detect_runtime(dir.path());
476 assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
477 }
478
479 #[test]
480 fn test_detect_wasm_cargo_config_wasip1_module() {
481 let dir = create_temp_dir();
482 fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
483 fs::create_dir_all(dir.path().join(".cargo")).unwrap();
484 fs::write(
485 dir.path().join(".cargo").join("config.toml"),
486 "[build]\ntarget = \"wasm32-wasip1\"\n",
487 )
488 .unwrap();
489
490 let runtime = detect_runtime(dir.path());
491 assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Module)));
492 }
493
494 #[test]
495 fn test_plain_rust_project_is_not_wasm() {
496 let dir = create_temp_dir();
499 fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
500
501 let runtime = detect_runtime(dir.path());
502 assert_eq!(runtime, Some(Runtime::Rust));
503 }
504
505 #[test]
506 fn test_plain_node_project_is_not_wasm() {
507 let dir = create_temp_dir();
509 fs::write(
510 dir.path().join("package.json"),
511 r#"{"dependencies":{"express":"^4"}}"#,
512 )
513 .unwrap();
514
515 let runtime = detect_runtime(dir.path());
516 assert_eq!(runtime, Some(Runtime::Node20));
517 }
518}