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 has_dotnet_project_files(path) {
46 return Some(Runtime::WindowsServerCore);
47 }
48
49 if path.join("package.json").exists() {
51 if path.join("bun.lockb").exists() {
53 return Some(Runtime::Bun);
54 }
55
56 if path.join("deno.json").exists() || path.join("deno.jsonc").exists() {
58 return Some(Runtime::Deno);
59 }
60
61 return Some(Runtime::Node20);
63 }
64
65 if path.join("deno.json").exists()
67 || path.join("deno.jsonc").exists()
68 || path.join("deno.lock").exists()
69 {
70 return Some(Runtime::Deno);
71 }
72
73 if path.join("Cargo.toml").exists() {
75 return Some(Runtime::Rust);
76 }
77
78 if path.join("pyproject.toml").exists()
80 || path.join("requirements.txt").exists()
81 || path.join("setup.py").exists()
82 || path.join("Pipfile").exists()
83 || path.join("poetry.lock").exists()
84 {
85 return Some(Runtime::Python312);
86 }
87
88 if path.join("go.mod").exists() {
90 return Some(Runtime::Go);
91 }
92
93 if has_windows_exe(path) {
98 return Some(Runtime::WindowsNanoserver);
99 }
100
101 None
102}
103
104fn has_dotnet_project_files(path: &Path) -> bool {
107 if path.join("project.json").exists() {
108 return true;
109 }
110 dir_has_extension(path, &["sln", "csproj", "vcxproj"])
111}
112
113fn has_windows_exe(path: &Path) -> bool {
117 dir_has_extension(path, &["exe"])
118}
119
120fn dir_has_extension(path: &Path, extensions: &[&str]) -> bool {
124 let Ok(entries) = std::fs::read_dir(path) else {
125 return false;
126 };
127 for entry in entries.flatten() {
128 let Ok(file_type) = entry.file_type() else {
129 continue;
130 };
131 if !file_type.is_file() {
132 continue;
133 }
134 let file_name = entry.file_name();
135 let name = file_name.to_string_lossy();
136 if let Some((_, ext)) = name.rsplit_once('.') {
137 let ext_lower = ext.to_ascii_lowercase();
138 if extensions
139 .iter()
140 .any(|&e| e.eq_ignore_ascii_case(&ext_lower))
141 {
142 return true;
143 }
144 }
145 }
146 false
147}
148
149pub fn detect_runtime_with_version(context_path: impl AsRef<Path>) -> Option<Runtime> {
154 let path = context_path.as_ref();
155
156 let base_runtime = detect_runtime(path)?;
158
159 match base_runtime {
161 Runtime::Node20 | Runtime::Node22 => {
162 if let Some(version) = read_node_version(path) {
164 if version.starts_with("22") || version.starts_with("v22") {
165 return Some(Runtime::Node22);
166 }
167 if version.starts_with("20") || version.starts_with("v20") {
168 return Some(Runtime::Node20);
169 }
170 }
171
172 if let Some(version) = read_package_node_version(path) {
174 if version.contains("22") {
175 return Some(Runtime::Node22);
176 }
177 }
178
179 Some(Runtime::Node20)
180 }
181 Runtime::Python312 | Runtime::Python313 => {
182 if let Some(version) = read_python_version(path) {
184 if version.starts_with("3.13") {
185 return Some(Runtime::Python313);
186 }
187 }
188
189 Some(Runtime::Python312)
190 }
191 other => Some(other),
192 }
193}
194
195fn detect_wasm_hint(path: &Path) -> Option<WasmTargetHint> {
209 if path.join("cargo-component.toml").exists() {
211 return Some(WasmTargetHint::Component);
212 }
213
214 let cargo_toml = path.join("Cargo.toml");
218 let cargo_has_component_metadata = if cargo_toml.exists() {
219 cargo_toml_has_component_metadata(&cargo_toml)
220 } else {
221 false
222 };
223 if cargo_has_component_metadata {
224 return Some(WasmTargetHint::Component);
225 }
226
227 if path.join("componentize-py.config").exists() {
229 return Some(WasmTargetHint::Component);
230 }
231
232 if path.join("package.json").exists() && package_json_uses_jco(&path.join("package.json")) {
234 return Some(WasmTargetHint::Component);
235 }
236
237 if cargo_toml.exists() && cargo_config_targets_wasip(path) {
239 return Some(WasmTargetHint::Module);
240 }
241
242 None
243}
244
245fn cargo_toml_has_component_metadata(cargo_toml: &Path) -> bool {
250 let Ok(content) = std::fs::read_to_string(cargo_toml) else {
251 return false;
252 };
253 content.lines().any(|line| {
254 let trimmed = line.trim();
255 trimmed == "[package.metadata.component]"
256 || trimmed.starts_with("[package.metadata.component.")
257 })
258}
259
260const JCO_DEP_SECTIONS: &[&str] = &[
262 "dependencies",
263 "devDependencies",
264 "peerDependencies",
265 "optionalDependencies",
266];
267
268fn package_json_uses_jco(package_json: &Path) -> bool {
271 let Ok(content) = std::fs::read_to_string(package_json) else {
272 return false;
273 };
274 let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
275 return false;
276 };
277
278 for section in JCO_DEP_SECTIONS {
279 if let Some(obj) = json.get(section).and_then(|v| v.as_object()) {
280 if obj.contains_key("jco") || obj.contains_key("@bytecodealliance/jco") {
281 return true;
282 }
283 }
284 }
285 false
286}
287
288fn cargo_config_targets_wasip(path: &Path) -> bool {
291 let config = path.join(".cargo").join("config.toml");
292 let Ok(content) = std::fs::read_to_string(&config) else {
293 return false;
294 };
295 content.lines().any(|line| {
296 let trimmed = line.trim();
297 trimmed.contains("wasm32-wasip1") || trimmed.contains("wasm32-wasip2")
298 })
299}
300
301fn read_node_version(path: &Path) -> Option<String> {
303 for filename in &[".nvmrc", ".node-version"] {
304 let version_file = path.join(filename);
305 if version_file.exists() {
306 if let Ok(content) = std::fs::read_to_string(&version_file) {
307 let version = content.trim().to_string();
308 if !version.is_empty() {
309 return Some(version);
310 }
311 }
312 }
313 }
314 None
315}
316
317fn read_package_node_version(path: &Path) -> Option<String> {
319 let package_json = path.join("package.json");
320 if package_json.exists() {
321 if let Ok(content) = std::fs::read_to_string(&package_json) {
322 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
323 if let Some(engines) = json.get("engines") {
324 if let Some(node) = engines.get("node") {
325 if let Some(version) = node.as_str() {
326 return Some(version.to_string());
327 }
328 }
329 }
330 }
331 }
332 }
333 None
334}
335
336fn read_python_version(path: &Path) -> Option<String> {
338 let python_version = path.join(".python-version");
340 if python_version.exists() {
341 if let Ok(content) = std::fs::read_to_string(&python_version) {
342 let version = content.trim().to_string();
343 if !version.is_empty() {
344 return Some(version);
345 }
346 }
347 }
348
349 let pyproject = path.join("pyproject.toml");
351 if pyproject.exists() {
352 if let Ok(content) = std::fs::read_to_string(&pyproject) {
353 for line in content.lines() {
355 let line = line.trim();
356 if line.starts_with("requires-python") {
357 if let Some(version) = line.split('=').nth(1) {
358 let version = version.trim().trim_matches('"').trim_matches('\'');
359 return Some(version.to_string());
360 }
361 }
362 }
363 }
364 }
365
366 None
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use std::fs;
373 use tempfile::TempDir;
374
375 fn create_temp_dir() -> TempDir {
376 TempDir::new().expect("Failed to create temp directory")
377 }
378
379 #[test]
380 fn test_detect_nodejs_project() {
381 let dir = create_temp_dir();
382 fs::write(dir.path().join("package.json"), "{}").unwrap();
383
384 let runtime = detect_runtime(dir.path());
385 assert_eq!(runtime, Some(Runtime::Node20));
386 }
387
388 #[test]
389 fn test_detect_bun_project() {
390 let dir = create_temp_dir();
391 fs::write(dir.path().join("package.json"), "{}").unwrap();
392 fs::write(dir.path().join("bun.lockb"), "").unwrap();
393
394 let runtime = detect_runtime(dir.path());
395 assert_eq!(runtime, Some(Runtime::Bun));
396 }
397
398 #[test]
399 fn test_detect_deno_project() {
400 let dir = create_temp_dir();
401 fs::write(dir.path().join("deno.json"), "{}").unwrap();
402
403 let runtime = detect_runtime(dir.path());
404 assert_eq!(runtime, Some(Runtime::Deno));
405 }
406
407 #[test]
408 fn test_detect_deno_with_package_json() {
409 let dir = create_temp_dir();
410 fs::write(dir.path().join("package.json"), "{}").unwrap();
411 fs::write(dir.path().join("deno.json"), "{}").unwrap();
412
413 let runtime = detect_runtime(dir.path());
414 assert_eq!(runtime, Some(Runtime::Deno));
415 }
416
417 #[test]
418 fn test_detect_rust_project() {
419 let dir = create_temp_dir();
420 fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
421
422 let runtime = detect_runtime(dir.path());
423 assert_eq!(runtime, Some(Runtime::Rust));
424 }
425
426 #[test]
427 fn test_detect_python_requirements() {
428 let dir = create_temp_dir();
429 fs::write(dir.path().join("requirements.txt"), "flask==2.0").unwrap();
430
431 let runtime = detect_runtime(dir.path());
432 assert_eq!(runtime, Some(Runtime::Python312));
433 }
434
435 #[test]
436 fn test_detect_python_pyproject() {
437 let dir = create_temp_dir();
438 fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
439
440 let runtime = detect_runtime(dir.path());
441 assert_eq!(runtime, Some(Runtime::Python312));
442 }
443
444 #[test]
445 fn test_detect_go_project() {
446 let dir = create_temp_dir();
447 fs::write(dir.path().join("go.mod"), "module example.com/app").unwrap();
448
449 let runtime = detect_runtime(dir.path());
450 assert_eq!(runtime, Some(Runtime::Go));
451 }
452
453 #[test]
454 fn test_detect_no_runtime() {
455 let dir = create_temp_dir();
456 let runtime = detect_runtime(dir.path());
459 assert_eq!(runtime, None);
460 }
461
462 #[test]
463 fn test_detect_node22_from_nvmrc() {
464 let dir = create_temp_dir();
465 fs::write(dir.path().join("package.json"), "{}").unwrap();
466 fs::write(dir.path().join(".nvmrc"), "22.0.0").unwrap();
467
468 let runtime = detect_runtime_with_version(dir.path());
469 assert_eq!(runtime, Some(Runtime::Node22));
470 }
471
472 #[test]
473 fn test_detect_node22_from_package_engines() {
474 let dir = create_temp_dir();
475 let package_json = r#"{"engines": {"node": ">=22.0.0"}}"#;
476 fs::write(dir.path().join("package.json"), package_json).unwrap();
477
478 let runtime = detect_runtime_with_version(dir.path());
479 assert_eq!(runtime, Some(Runtime::Node22));
480 }
481
482 #[test]
483 fn test_detect_python313_from_version_file() {
484 let dir = create_temp_dir();
485 fs::write(dir.path().join("requirements.txt"), "flask").unwrap();
486 fs::write(dir.path().join(".python-version"), "3.13.0").unwrap();
487
488 let runtime = detect_runtime_with_version(dir.path());
489 assert_eq!(runtime, Some(Runtime::Python313));
490 }
491
492 #[test]
495 fn test_detect_wasm_cargo_component_toml() {
496 let dir = create_temp_dir();
497 fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
498 fs::write(dir.path().join("cargo-component.toml"), "").unwrap();
499
500 let runtime = detect_runtime(dir.path());
501 assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
502 }
503
504 #[test]
505 fn test_detect_wasm_cargo_metadata_component() {
506 let dir = create_temp_dir();
507 let toml = r#"
508[package]
509name = "foo"
510version = "0.1.0"
511
512[package.metadata.component]
513package = "zlayer:example"
514"#;
515 fs::write(dir.path().join("Cargo.toml"), toml).unwrap();
516
517 let runtime = detect_runtime(dir.path());
518 assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
519 }
520
521 #[test]
522 fn test_detect_wasm_componentize_py() {
523 let dir = create_temp_dir();
524 fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
525 fs::write(dir.path().join("componentize-py.config"), "").unwrap();
526
527 let runtime = detect_runtime(dir.path());
528 assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
529 }
530
531 #[test]
532 fn test_detect_wasm_jco_in_package_json() {
533 let dir = create_temp_dir();
534 let pkg = r#"{
535 "name": "foo",
536 "devDependencies": { "@bytecodealliance/jco": "^1.0.0" }
537}"#;
538 fs::write(dir.path().join("package.json"), pkg).unwrap();
539
540 let runtime = detect_runtime(dir.path());
541 assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Component)));
542 }
543
544 #[test]
545 fn test_detect_wasm_cargo_config_wasip1_module() {
546 let dir = create_temp_dir();
547 fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
548 fs::create_dir_all(dir.path().join(".cargo")).unwrap();
549 fs::write(
550 dir.path().join(".cargo").join("config.toml"),
551 "[build]\ntarget = \"wasm32-wasip1\"\n",
552 )
553 .unwrap();
554
555 let runtime = detect_runtime(dir.path());
556 assert_eq!(runtime, Some(Runtime::Wasm(WasmTargetHint::Module)));
557 }
558
559 #[test]
560 fn test_plain_rust_project_is_not_wasm() {
561 let dir = create_temp_dir();
564 fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
565
566 let runtime = detect_runtime(dir.path());
567 assert_eq!(runtime, Some(Runtime::Rust));
568 }
569
570 #[test]
571 fn test_plain_node_project_is_not_wasm() {
572 let dir = create_temp_dir();
574 fs::write(
575 dir.path().join("package.json"),
576 r#"{"dependencies":{"express":"^4"}}"#,
577 )
578 .unwrap();
579
580 let runtime = detect_runtime(dir.path());
581 assert_eq!(runtime, Some(Runtime::Node20));
582 }
583
584 #[test]
587 fn test_detect_windows_servercore_from_sln() {
588 let dir = create_temp_dir();
589 fs::write(dir.path().join("MyApp.sln"), "").unwrap();
590
591 let runtime = detect_runtime(dir.path());
592 assert_eq!(runtime, Some(Runtime::WindowsServerCore));
593 }
594
595 #[test]
596 fn test_detect_windows_servercore_from_csproj() {
597 let dir = create_temp_dir();
598 fs::write(
599 dir.path().join("MyApp.csproj"),
600 r#"<Project Sdk="Microsoft.NET.Sdk" />"#,
601 )
602 .unwrap();
603
604 let runtime = detect_runtime(dir.path());
605 assert_eq!(runtime, Some(Runtime::WindowsServerCore));
606 }
607
608 #[test]
609 fn test_detect_windows_servercore_from_vcxproj() {
610 let dir = create_temp_dir();
611 fs::write(dir.path().join("Native.vcxproj"), "").unwrap();
612
613 let runtime = detect_runtime(dir.path());
614 assert_eq!(runtime, Some(Runtime::WindowsServerCore));
615 }
616
617 #[test]
618 fn test_detect_windows_servercore_from_legacy_project_json() {
619 let dir = create_temp_dir();
620 fs::write(dir.path().join("project.json"), "{}").unwrap();
623
624 let runtime = detect_runtime(dir.path());
625 assert_eq!(runtime, Some(Runtime::WindowsServerCore));
626 }
627
628 #[test]
629 fn test_detect_windows_nanoserver_from_standalone_exe() {
630 let dir = create_temp_dir();
631 fs::write(dir.path().join("app.exe"), b"MZ\x90\x00").unwrap();
632
633 let runtime = detect_runtime(dir.path());
634 assert_eq!(runtime, Some(Runtime::WindowsNanoserver));
635 }
636
637 #[test]
638 fn test_detect_case_insensitive_exe_extension() {
639 let dir = create_temp_dir();
640 fs::write(dir.path().join("App.EXE"), b"MZ").unwrap();
642
643 let runtime = detect_runtime(dir.path());
644 assert_eq!(runtime, Some(Runtime::WindowsNanoserver));
645 }
646
647 #[test]
648 fn test_dotnet_wins_over_linux_python_hint() {
649 let dir = create_temp_dir();
653 fs::write(dir.path().join("MyApp.sln"), "").unwrap();
654 fs::write(dir.path().join("requirements.txt"), "requests").unwrap();
655
656 let runtime = detect_runtime(dir.path());
657 assert_eq!(runtime, Some(Runtime::WindowsServerCore));
658 }
659
660 #[test]
661 fn test_exe_does_not_override_rust_project() {
662 let dir = create_temp_dir();
665 fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
666 fs::write(dir.path().join("helper.exe"), b"MZ").unwrap();
667
668 let runtime = detect_runtime(dir.path());
669 assert_eq!(runtime, Some(Runtime::Rust));
670 }
671
672 #[test]
673 fn test_exe_does_not_override_go_project() {
674 let dir = create_temp_dir();
675 fs::write(dir.path().join("go.mod"), "module example.com/app").unwrap();
676 fs::write(dir.path().join("helper.exe"), b"MZ").unwrap();
677
678 let runtime = detect_runtime(dir.path());
679 assert_eq!(runtime, Some(Runtime::Go));
680 }
681
682 #[test]
683 fn test_exe_does_not_override_node_project() {
684 let dir = create_temp_dir();
685 fs::write(dir.path().join("package.json"), "{}").unwrap();
686 fs::write(dir.path().join("helper.exe"), b"MZ").unwrap();
687
688 let runtime = detect_runtime(dir.path());
689 assert_eq!(runtime, Some(Runtime::Node20));
690 }
691}