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