1use std::path::{Path, PathBuf};
7
8use log::warn;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum BuildSystem {
14 Gradle,
15 Maven,
16 Bazel,
17 Sbt,
18}
19
20impl BuildSystem {
21 fn from_str_loose(s: &str) -> Option<Self> {
25 match s.to_lowercase().as_str() {
26 "gradle" => Some(Self::Gradle),
27 "maven" => Some(Self::Maven),
28 "bazel" => Some(Self::Bazel),
29 "sbt" => Some(Self::Sbt),
30 _ => None,
31 }
32 }
33
34 fn priority(self) -> u8 {
39 match self {
40 Self::Bazel => 4,
41 Self::Gradle => 3,
42 Self::Maven => 2,
43 Self::Sbt => 1,
44 }
45 }
46
47 fn markers(self) -> &'static [&'static str] {
49 match self {
50 Self::Gradle => &[
51 "build.gradle",
52 "build.gradle.kts",
53 "settings.gradle",
54 "settings.gradle.kts",
55 "gradlew",
56 ],
57 Self::Maven => &["pom.xml"],
58 Self::Bazel => &[
59 "BUILD",
60 "BUILD.bazel",
61 "WORKSPACE",
62 "WORKSPACE.bazel",
63 "MODULE.bazel",
64 ],
65 Self::Sbt => &["build.sbt", "project/build.properties"],
66 }
67 }
68
69 const ALL: [Self; 4] = [Self::Gradle, Self::Maven, Self::Bazel, Self::Sbt];
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct DetectionResult {
76 pub build_system: Option<BuildSystem>,
78 pub project_root: PathBuf,
80 pub markers_found: Vec<String>,
82 pub override_source: Option<String>,
84}
85
86#[must_use]
97pub fn detect_build_system(
98 project_root: &Path,
99 override_build_system: Option<&str>,
100) -> DetectionResult {
101 if let Some(override_value) = override_build_system {
103 let result = if let Some(bs) = BuildSystem::from_str_loose(override_value) {
104 DetectionResult {
105 build_system: Some(bs),
106 project_root: project_root.to_path_buf(),
107 markers_found: Vec::new(),
108 override_source: Some(override_value.to_string()),
109 }
110 } else {
111 warn!(
112 "Invalid build system override '{override_value}'. Valid values: gradle, maven, bazel, sbt"
113 );
114 DetectionResult {
115 build_system: None,
116 project_root: project_root.to_path_buf(),
117 markers_found: Vec::new(),
118 override_source: Some(override_value.to_string()),
119 }
120 };
121 write_diagnostics(project_root, &result);
122 return result;
123 }
124
125 let mut markers_found = Vec::new();
127 let mut best_system: Option<BuildSystem> = None;
128
129 for build_system in BuildSystem::ALL {
130 for marker in build_system.markers() {
131 let marker_path = project_root.join(marker);
132 if marker_path.exists() {
133 markers_found.push((*marker).to_string());
134
135 match best_system {
136 Some(current) if current.priority() >= build_system.priority() => {
137 }
139 _ => {
140 best_system = Some(build_system);
141 }
142 }
143 }
144 }
145 }
146
147 let result = DetectionResult {
148 build_system: best_system,
149 project_root: project_root.to_path_buf(),
150 markers_found,
151 override_source: None,
152 };
153
154 write_diagnostics(project_root, &result);
155 result
156}
157
158fn write_diagnostics(project_root: &Path, result: &DetectionResult) {
161 let sqry_dir = project_root.join(".sqry").join("classpath");
162
163 if let Err(e) = std::fs::create_dir_all(&sqry_dir) {
165 warn!(
166 "Could not create diagnostics directory {}: {}",
167 sqry_dir.display(),
168 e
169 );
170 return;
171 }
172
173 let diagnostics_path = sqry_dir.join("build-system.json");
174 match serde_json::to_string_pretty(result) {
175 Ok(json) => {
176 if let Err(e) = std::fs::write(&diagnostics_path, json) {
177 warn!(
178 "Could not write build system diagnostics to {}: {}",
179 diagnostics_path.display(),
180 e
181 );
182 }
183 }
184 Err(e) => {
185 warn!("Could not serialize detection result: {e}");
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use tempfile::TempDir;
194
195 fn create_markers(dir: &Path, markers: &[&str]) {
197 for marker in markers {
198 let path = dir.join(marker);
199 if let Some(parent) = path.parent() {
201 std::fs::create_dir_all(parent).unwrap();
202 }
203 std::fs::write(&path, "").unwrap();
204 }
205 }
206
207 #[test]
208 fn test_build_gradle_detected() {
209 let tmp = TempDir::new().unwrap();
210 create_markers(tmp.path(), &["build.gradle"]);
211
212 let result = detect_build_system(tmp.path(), None);
213 assert_eq!(result.build_system, Some(BuildSystem::Gradle));
214 assert!(result.markers_found.contains(&"build.gradle".to_string()));
215 assert!(result.override_source.is_none());
216 }
217
218 #[test]
219 fn test_pom_xml_only_maven() {
220 let tmp = TempDir::new().unwrap();
221 create_markers(tmp.path(), &["pom.xml"]);
222
223 let result = detect_build_system(tmp.path(), None);
224 assert_eq!(result.build_system, Some(BuildSystem::Maven));
225 assert!(result.markers_found.contains(&"pom.xml".to_string()));
226 }
227
228 #[test]
229 fn test_build_and_pom_bazel_wins() {
230 let tmp = TempDir::new().unwrap();
231 create_markers(tmp.path(), &["BUILD", "pom.xml"]);
232
233 let result = detect_build_system(tmp.path(), None);
234 assert_eq!(result.build_system, Some(BuildSystem::Bazel));
235 assert!(result.markers_found.contains(&"BUILD".to_string()));
236 assert!(result.markers_found.contains(&"pom.xml".to_string()));
237 }
238
239 #[test]
240 fn test_build_sbt_only() {
241 let tmp = TempDir::new().unwrap();
242 create_markers(tmp.path(), &["build.sbt"]);
243
244 let result = detect_build_system(tmp.path(), None);
245 assert_eq!(result.build_system, Some(BuildSystem::Sbt));
246 assert!(result.markers_found.contains(&"build.sbt".to_string()));
247 }
248
249 #[test]
250 fn test_no_markers_none() {
251 let tmp = TempDir::new().unwrap();
252
253 let result = detect_build_system(tmp.path(), None);
254 assert_eq!(result.build_system, None);
255 assert!(result.markers_found.is_empty());
256 }
257
258 #[test]
259 fn test_override_works() {
260 let tmp = TempDir::new().unwrap();
261 create_markers(tmp.path(), &["pom.xml"]);
263
264 let result = detect_build_system(tmp.path(), Some("gradle"));
265 assert_eq!(result.build_system, Some(BuildSystem::Gradle));
266 assert_eq!(result.override_source, Some("gradle".to_string()));
267 assert!(result.markers_found.is_empty());
269 }
270
271 #[test]
272 fn test_invalid_override_returns_none() {
273 let tmp = TempDir::new().unwrap();
274
275 let result = detect_build_system(tmp.path(), Some("ninja"));
276 assert_eq!(result.build_system, None);
277 assert_eq!(result.override_source, Some("ninja".to_string()));
278 }
279
280 #[test]
281 fn test_all_markers_bazel_wins() {
282 let tmp = TempDir::new().unwrap();
283 create_markers(
284 tmp.path(),
285 &["build.gradle", "pom.xml", "BUILD", "build.sbt", "WORKSPACE"],
286 );
287
288 let result = detect_build_system(tmp.path(), None);
289 assert_eq!(result.build_system, Some(BuildSystem::Bazel));
290 assert!(result.markers_found.len() >= 4);
292 }
293
294 #[test]
295 fn test_build_gradle_kts_detected() {
296 let tmp = TempDir::new().unwrap();
297 create_markers(tmp.path(), &["build.gradle.kts"]);
298
299 let result = detect_build_system(tmp.path(), None);
300 assert_eq!(result.build_system, Some(BuildSystem::Gradle));
301 assert!(
302 result
303 .markers_found
304 .contains(&"build.gradle.kts".to_string())
305 );
306 }
307
308 #[test]
309 fn test_workspace_bazel_detected() {
310 let tmp = TempDir::new().unwrap();
311 create_markers(tmp.path(), &["WORKSPACE.bazel"]);
312
313 let result = detect_build_system(tmp.path(), None);
314 assert_eq!(result.build_system, Some(BuildSystem::Bazel));
315 assert!(
316 result
317 .markers_found
318 .contains(&"WORKSPACE.bazel".to_string())
319 );
320 }
321
322 #[test]
323 fn test_diagnostics_file_written() {
324 let tmp = TempDir::new().unwrap();
325 create_markers(tmp.path(), &["pom.xml"]);
326
327 let _result = detect_build_system(tmp.path(), None);
328
329 let diagnostics_path = tmp.path().join(".sqry/classpath/build-system.json");
330 assert!(diagnostics_path.exists(), "diagnostics file should exist");
331
332 let contents = std::fs::read_to_string(&diagnostics_path).unwrap();
333 let parsed: serde_json::Value = serde_json::from_str(&contents).unwrap();
334 assert_eq!(parsed["build_system"], "Maven");
335 }
336
337 #[test]
338 fn test_project_root_recorded() {
339 let tmp = TempDir::new().unwrap();
340 let result = detect_build_system(tmp.path(), None);
341 assert_eq!(result.project_root, tmp.path());
342 }
343
344 #[test]
345 fn test_override_case_insensitive() {
346 let tmp = TempDir::new().unwrap();
347
348 let result = detect_build_system(tmp.path(), Some("MAVEN"));
349 assert_eq!(result.build_system, Some(BuildSystem::Maven));
350
351 let result = detect_build_system(tmp.path(), Some("Gradle"));
352 assert_eq!(result.build_system, Some(BuildSystem::Gradle));
353
354 let result = detect_build_system(tmp.path(), Some("SBT"));
355 assert_eq!(result.build_system, Some(BuildSystem::Sbt));
356
357 let result = detect_build_system(tmp.path(), Some("BAZEL"));
358 assert_eq!(result.build_system, Some(BuildSystem::Bazel));
359 }
360
361 #[test]
362 fn test_settings_gradle_detected() {
363 let tmp = TempDir::new().unwrap();
364 create_markers(tmp.path(), &["settings.gradle"]);
365
366 let result = detect_build_system(tmp.path(), None);
367 assert_eq!(result.build_system, Some(BuildSystem::Gradle));
368 }
369
370 #[test]
371 fn test_settings_gradle_kts_detected() {
372 let tmp = TempDir::new().unwrap();
373 create_markers(tmp.path(), &["settings.gradle.kts"]);
374
375 let result = detect_build_system(tmp.path(), None);
376 assert_eq!(result.build_system, Some(BuildSystem::Gradle));
377 }
378
379 #[test]
380 fn test_gradlew_detected() {
381 let tmp = TempDir::new().unwrap();
382 create_markers(tmp.path(), &["gradlew"]);
383
384 let result = detect_build_system(tmp.path(), None);
385 assert_eq!(result.build_system, Some(BuildSystem::Gradle));
386 }
387
388 #[test]
389 fn test_module_bazel_detected() {
390 let tmp = TempDir::new().unwrap();
391 create_markers(tmp.path(), &["MODULE.bazel"]);
392
393 let result = detect_build_system(tmp.path(), None);
394 assert_eq!(result.build_system, Some(BuildSystem::Bazel));
395 }
396
397 #[test]
398 fn test_sbt_project_build_properties() {
399 let tmp = TempDir::new().unwrap();
400 create_markers(tmp.path(), &["project/build.properties"]);
401
402 let result = detect_build_system(tmp.path(), None);
403 assert_eq!(result.build_system, Some(BuildSystem::Sbt));
404 assert!(
405 result
406 .markers_found
407 .contains(&"project/build.properties".to_string())
408 );
409 }
410
411 #[test]
412 fn test_gradle_vs_maven_gradle_wins() {
413 let tmp = TempDir::new().unwrap();
414 create_markers(tmp.path(), &["build.gradle", "pom.xml"]);
415
416 let result = detect_build_system(tmp.path(), None);
417 assert_eq!(result.build_system, Some(BuildSystem::Gradle));
418 }
419
420 #[test]
421 fn test_gradle_vs_sbt_gradle_wins() {
422 let tmp = TempDir::new().unwrap();
423 create_markers(tmp.path(), &["build.gradle", "build.sbt"]);
424
425 let result = detect_build_system(tmp.path(), None);
426 assert_eq!(result.build_system, Some(BuildSystem::Gradle));
427 }
428
429 #[test]
430 fn test_maven_vs_sbt_maven_wins() {
431 let tmp = TempDir::new().unwrap();
432 create_markers(tmp.path(), &["pom.xml", "build.sbt"]);
433
434 let result = detect_build_system(tmp.path(), None);
435 assert_eq!(result.build_system, Some(BuildSystem::Maven));
436 }
437
438 #[test]
439 fn test_multiple_gradle_markers() {
440 let tmp = TempDir::new().unwrap();
441 create_markers(tmp.path(), &["build.gradle", "settings.gradle", "gradlew"]);
442
443 let result = detect_build_system(tmp.path(), None);
444 assert_eq!(result.build_system, Some(BuildSystem::Gradle));
445 assert_eq!(result.markers_found.len(), 3);
446 }
447
448 #[test]
449 fn test_multiple_bazel_markers() {
450 let tmp = TempDir::new().unwrap();
451 create_markers(
452 tmp.path(),
453 &["BUILD", "BUILD.bazel", "WORKSPACE", "MODULE.bazel"],
454 );
455
456 let result = detect_build_system(tmp.path(), None);
457 assert_eq!(result.build_system, Some(BuildSystem::Bazel));
458 assert_eq!(result.markers_found.len(), 4);
459 }
460}