zlayer_builder/templates/
detect.rs1use std::path::Path;
7
8use super::Runtime;
9
10pub fn detect_runtime(context_path: impl AsRef<Path>) -> Option<Runtime> {
27 let path = context_path.as_ref();
28
29 if path.join("package.json").exists() {
31 if path.join("bun.lockb").exists() {
33 return Some(Runtime::Bun);
34 }
35
36 if path.join("deno.json").exists() || path.join("deno.jsonc").exists() {
38 return Some(Runtime::Deno);
39 }
40
41 return Some(Runtime::Node20);
43 }
44
45 if path.join("deno.json").exists()
47 || path.join("deno.jsonc").exists()
48 || path.join("deno.lock").exists()
49 {
50 return Some(Runtime::Deno);
51 }
52
53 if path.join("Cargo.toml").exists() {
55 return Some(Runtime::Rust);
56 }
57
58 if path.join("pyproject.toml").exists()
60 || path.join("requirements.txt").exists()
61 || path.join("setup.py").exists()
62 || path.join("Pipfile").exists()
63 || path.join("poetry.lock").exists()
64 {
65 return Some(Runtime::Python312);
66 }
67
68 if path.join("go.mod").exists() {
70 return Some(Runtime::Go);
71 }
72
73 None
74}
75
76pub fn detect_runtime_with_version(context_path: impl AsRef<Path>) -> Option<Runtime> {
81 let path = context_path.as_ref();
82
83 let base_runtime = detect_runtime(path)?;
85
86 match base_runtime {
88 Runtime::Node20 | Runtime::Node22 => {
89 if let Some(version) = read_node_version(path) {
91 if version.starts_with("22") || version.starts_with("v22") {
92 return Some(Runtime::Node22);
93 }
94 if version.starts_with("20") || version.starts_with("v20") {
95 return Some(Runtime::Node20);
96 }
97 }
98
99 if let Some(version) = read_package_node_version(path) {
101 if version.contains("22") {
102 return Some(Runtime::Node22);
103 }
104 }
105
106 Some(Runtime::Node20)
107 }
108 Runtime::Python312 | Runtime::Python313 => {
109 if let Some(version) = read_python_version(path) {
111 if version.starts_with("3.13") {
112 return Some(Runtime::Python313);
113 }
114 }
115
116 Some(Runtime::Python312)
117 }
118 other => Some(other),
119 }
120}
121
122fn read_node_version(path: &Path) -> Option<String> {
124 for filename in &[".nvmrc", ".node-version"] {
125 let version_file = path.join(filename);
126 if version_file.exists() {
127 if let Ok(content) = std::fs::read_to_string(&version_file) {
128 let version = content.trim().to_string();
129 if !version.is_empty() {
130 return Some(version);
131 }
132 }
133 }
134 }
135 None
136}
137
138fn read_package_node_version(path: &Path) -> Option<String> {
140 let package_json = path.join("package.json");
141 if package_json.exists() {
142 if let Ok(content) = std::fs::read_to_string(&package_json) {
143 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
144 if let Some(engines) = json.get("engines") {
145 if let Some(node) = engines.get("node") {
146 if let Some(version) = node.as_str() {
147 return Some(version.to_string());
148 }
149 }
150 }
151 }
152 }
153 }
154 None
155}
156
157fn read_python_version(path: &Path) -> Option<String> {
159 let python_version = path.join(".python-version");
161 if python_version.exists() {
162 if let Ok(content) = std::fs::read_to_string(&python_version) {
163 let version = content.trim().to_string();
164 if !version.is_empty() {
165 return Some(version);
166 }
167 }
168 }
169
170 let pyproject = path.join("pyproject.toml");
172 if pyproject.exists() {
173 if let Ok(content) = std::fs::read_to_string(&pyproject) {
174 for line in content.lines() {
176 let line = line.trim();
177 if line.starts_with("requires-python") {
178 if let Some(version) = line.split('=').nth(1) {
179 let version = version.trim().trim_matches('"').trim_matches('\'');
180 return Some(version.to_string());
181 }
182 }
183 }
184 }
185 }
186
187 None
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use std::fs;
194 use tempfile::TempDir;
195
196 fn create_temp_dir() -> TempDir {
197 TempDir::new().expect("Failed to create temp directory")
198 }
199
200 #[test]
201 fn test_detect_nodejs_project() {
202 let dir = create_temp_dir();
203 fs::write(dir.path().join("package.json"), "{}").unwrap();
204
205 let runtime = detect_runtime(dir.path());
206 assert_eq!(runtime, Some(Runtime::Node20));
207 }
208
209 #[test]
210 fn test_detect_bun_project() {
211 let dir = create_temp_dir();
212 fs::write(dir.path().join("package.json"), "{}").unwrap();
213 fs::write(dir.path().join("bun.lockb"), "").unwrap();
214
215 let runtime = detect_runtime(dir.path());
216 assert_eq!(runtime, Some(Runtime::Bun));
217 }
218
219 #[test]
220 fn test_detect_deno_project() {
221 let dir = create_temp_dir();
222 fs::write(dir.path().join("deno.json"), "{}").unwrap();
223
224 let runtime = detect_runtime(dir.path());
225 assert_eq!(runtime, Some(Runtime::Deno));
226 }
227
228 #[test]
229 fn test_detect_deno_with_package_json() {
230 let dir = create_temp_dir();
231 fs::write(dir.path().join("package.json"), "{}").unwrap();
232 fs::write(dir.path().join("deno.json"), "{}").unwrap();
233
234 let runtime = detect_runtime(dir.path());
235 assert_eq!(runtime, Some(Runtime::Deno));
236 }
237
238 #[test]
239 fn test_detect_rust_project() {
240 let dir = create_temp_dir();
241 fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
242
243 let runtime = detect_runtime(dir.path());
244 assert_eq!(runtime, Some(Runtime::Rust));
245 }
246
247 #[test]
248 fn test_detect_python_requirements() {
249 let dir = create_temp_dir();
250 fs::write(dir.path().join("requirements.txt"), "flask==2.0").unwrap();
251
252 let runtime = detect_runtime(dir.path());
253 assert_eq!(runtime, Some(Runtime::Python312));
254 }
255
256 #[test]
257 fn test_detect_python_pyproject() {
258 let dir = create_temp_dir();
259 fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
260
261 let runtime = detect_runtime(dir.path());
262 assert_eq!(runtime, Some(Runtime::Python312));
263 }
264
265 #[test]
266 fn test_detect_go_project() {
267 let dir = create_temp_dir();
268 fs::write(dir.path().join("go.mod"), "module example.com/app").unwrap();
269
270 let runtime = detect_runtime(dir.path());
271 assert_eq!(runtime, Some(Runtime::Go));
272 }
273
274 #[test]
275 fn test_detect_no_runtime() {
276 let dir = create_temp_dir();
277 let runtime = detect_runtime(dir.path());
280 assert_eq!(runtime, None);
281 }
282
283 #[test]
284 fn test_detect_node22_from_nvmrc() {
285 let dir = create_temp_dir();
286 fs::write(dir.path().join("package.json"), "{}").unwrap();
287 fs::write(dir.path().join(".nvmrc"), "22.0.0").unwrap();
288
289 let runtime = detect_runtime_with_version(dir.path());
290 assert_eq!(runtime, Some(Runtime::Node22));
291 }
292
293 #[test]
294 fn test_detect_node22_from_package_engines() {
295 let dir = create_temp_dir();
296 let package_json = r#"{"engines": {"node": ">=22.0.0"}}"#;
297 fs::write(dir.path().join("package.json"), package_json).unwrap();
298
299 let runtime = detect_runtime_with_version(dir.path());
300 assert_eq!(runtime, Some(Runtime::Node22));
301 }
302
303 #[test]
304 fn test_detect_python313_from_version_file() {
305 let dir = create_temp_dir();
306 fs::write(dir.path().join("requirements.txt"), "flask").unwrap();
307 fs::write(dir.path().join(".python-version"), "3.13.0").unwrap();
308
309 let runtime = detect_runtime_with_version(dir.path());
310 assert_eq!(runtime, Some(Runtime::Python313));
311 }
312}