diff --git a/bazel-jdt-bridge/crates/bazel-aspect/src/ide_info.rs b/bazel-jdt-bridge/crates/bazel-aspect/src/ide_info.rs index 2adb4c8..8868204 100644 --- a/bazel-jdt-bridge/crates/bazel-aspect/src/ide_info.rs +++ b/bazel-jdt-bridge/crates/bazel-aspect/src/ide_info.rs @@ -159,4 +159,45 @@ mod tests { fn test_platforms_returns_none() { assert_eq!(canonical_to_apparent_label("@@platforms//cpu:cpu"), None); } + + use super::ArtifactLocation; + + #[test] + fn test_best_path_prefers_absolute_path() { + let loc = ArtifactLocation { + absolute_path: Some("/execroot/ws/external/maven/v1/lib.jar".to_string()), + root_path: Some("external/maven/v1".to_string()), + relative_path: Some("lib.jar".to_string()), + ..Default::default() + }; + assert_eq!( + loc.best_path(), + Some("/execroot/ws/external/maven/v1/lib.jar".to_string()) + ); + } + + #[test] + fn test_best_path_falls_back_to_root_plus_relative() { + let loc = ArtifactLocation { + absolute_path: None, + root_path: Some("bazel-out/k8-fastbuild/bin".to_string()), + relative_path: Some("app/libapp.jar".to_string()), + ..Default::default() + }; + assert_eq!( + loc.best_path(), + Some("bazel-out/k8-fastbuild/bin/app/libapp.jar".to_string()) + ); + } + + #[test] + fn test_best_path_relative_only() { + let loc = ArtifactLocation { + absolute_path: None, + root_path: None, + relative_path: Some("src/Main.java".to_string()), + ..Default::default() + }; + assert_eq!(loc.best_path(), Some("src/Main.java".to_string())); + } } diff --git a/bazel-jdt-bridge/crates/bazel-aspect/src/text_proto.rs b/bazel-jdt-bridge/crates/bazel-aspect/src/text_proto.rs index ec493a4..aba2bac 100644 --- a/bazel-jdt-bridge/crates/bazel-aspect/src/text_proto.rs +++ b/bazel-jdt-bridge/crates/bazel-aspect/src/text_proto.rs @@ -1050,4 +1050,55 @@ deps { Some("bazel-out/bin/foo/bar-src.jar") ); } + + #[test] + fn test_parse_absolute_path_in_artifact_location() { + let input = r#" + label: "//app:app" + kind_string: "java_library" + java_ide_info { + jars { + jar { + relative_path: "lib.jar" + root_path: "external/maven/v1" + absolute_path: "external/maven/v1/lib.jar" + is_source: false + is_external: true + } + } + } +"#; + let result = parse_text_proto(input); + let jar_loc = &result.value.java_info.as_ref().unwrap().jars[0].jar; + assert_eq!( + jar_loc.absolute_path.as_deref(), + Some("external/maven/v1/lib.jar") + ); + assert_eq!(jar_loc.relative_path.as_deref(), Some("lib.jar")); + assert_eq!(jar_loc.root_path.as_deref(), Some("external/maven/v1")); + } + + #[test] + fn test_parse_legacy_no_absolute_path() { + let input = r#" + label: "//app:app" + kind_string: "java_library" + java_ide_info { + jars { + jar { + relative_path: "lib.jar" + root_path: "external/maven/v1" + is_source: false + } + } + } +"#; + let result = parse_text_proto(input); + let jar_loc = &result.value.java_info.as_ref().unwrap().jars[0].jar; + assert!( + jar_loc.absolute_path.is_none(), + "Legacy output without absolute_path should parse as None" + ); + assert_eq!(jar_loc.relative_path.as_deref(), Some("lib.jar")); + } } diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs index 0bb5443..ac46d1e 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs @@ -84,9 +84,13 @@ impl ComputedClasspath { TargetKind::JavaLibrary | TargetKind::JavaBinary | TargetKind::JavaTest - | TargetKind::Unknown => { - Self::compute_for_library(graph, target_label, is_test, workspace_root) - } + | TargetKind::Unknown => Self::compute_for_library( + graph, + target_label, + is_test, + workspace_root, + &target_kind, + ), } } @@ -176,6 +180,7 @@ impl ComputedClasspath { target_label: &str, is_test_context: bool, workspace_root: Option<&str>, + target_kind: &TargetKind, ) -> Result { let deps = graph.transitive_deps(target_label)?; @@ -220,7 +225,7 @@ impl ComputedClasspath { }) }; - if let Some(&existing_idx) = seen_jars.get(&jar.classpath_path) { + if let Some(&existing_idx) = seen_jars.get(jar.effective_path()) { if entries[existing_idx].source_attachment_path.is_none() { let source = resolve_source(jar); if source.is_some() { @@ -229,10 +234,10 @@ impl ComputedClasspath { } } else { let source = resolve_source(jar); - seen_jars.insert(jar.classpath_path.clone(), entries.len()); + seen_jars.insert(jar.effective_path().to_string(), entries.len()); entries.push(ClasspathEntry { entry_type: ClasspathEntryType::Library, - path: jar.classpath_path.clone(), + path: jar.effective_path().to_string(), source_attachment_path: source, is_test: dep_is_testonly, is_exported: false, @@ -244,11 +249,37 @@ impl ComputedClasspath { } } - let output_jars = graph + let mut output_jars: Vec = graph .get_target_jars(target_label) - .map(|jars| jars.iter().map(|j| j.classpath_path.clone()).collect()) + .map(|jars| { + jars.iter() + .map(|j| j.effective_path().to_string()) + .collect() + }) .unwrap_or_default(); + if *target_kind == TargetKind::JavaBinary { + for dep_label in &deps { + if is_bazel_internal_label(dep_label) { + continue; + } + if let Some(dep_jars) = graph.get_target_jars(dep_label) { + for jar in dep_jars { + if !output_jars.iter().any(|p| p == jar.effective_path()) { + output_jars.push(jar.effective_path().to_string()); + } + } + } + } + + if output_jars.len() > 1 { + output_jars.retain(|jar_path| match std::fs::metadata(jar_path) { + Ok(meta) => meta.len() >= 1024, + Err(_) => true, + }); + } + } + Ok(ComputedClasspath { target_label: target_label.to_string(), entries, @@ -277,7 +308,7 @@ impl ComputedClasspath { .cloned(); entries.push(ClasspathEntry { entry_type: ClasspathEntryType::Library, - path: jar.classpath_path.clone(), + path: jar.effective_path().to_string(), source_attachment_path: source, is_test: false, is_exported: false, @@ -289,7 +320,11 @@ impl ComputedClasspath { let output_jars = graph .get_target_jars(target_label) - .map(|jars| jars.iter().map(|j| j.classpath_path.clone()).collect()) + .map(|jars| { + jars.iter() + .map(|j| j.effective_path().to_string()) + .collect() + }) .unwrap_or_default(); Ok(ComputedClasspath { @@ -327,39 +362,51 @@ impl ComputedClasspath { .collect() } - /// Convert to pipe-delimited string array for JNI + /// Convert to pipe-delimited string array for JNI. + /// Output JARs (the target's own compiled JARs) are emitted first as LIB + /// entries so they appear on the runtime classpath, filling the gap left by + /// the empty Eclipse output location when JavaBuilder is disabled. pub fn to_pipe_delimited_entries(&self) -> Vec { - self.entries - .iter() - .map(|entry| { - let type_str = match entry.entry_type { - ClasspathEntryType::Library => "LIB", - ClasspathEntryType::Project => "PROJ", - ClasspathEntryType::Source => "SRC", - }; - let source = entry.source_attachment_path.as_deref().unwrap_or(""); - let access = if entry.access_rules.is_empty() { - "".to_string() - } else { - entry - .access_rules - .iter() - .map(|r| { - if r.is_accessible { - format!("+{}", r.pattern) - } else { - format!("-{}", r.pattern) - } - }) - .collect::>() - .join(":") - }; - format!( - "{}|{}|{}|{}|{}|{}", - type_str, entry.path, source, entry.is_test, entry.is_exported, access - ) - }) - .collect() + let mut result = Vec::with_capacity(self.output_jars.len() + self.entries.len()); + + let entry_paths: std::collections::HashSet<&str> = + self.entries.iter().map(|e| e.path.as_str()).collect(); + for jar_path in &self.output_jars { + if !entry_paths.contains(jar_path.as_str()) { + result.push(format!("LIB|{}||false|false|", jar_path)); + } + } + + for entry in &self.entries { + let type_str = match entry.entry_type { + ClasspathEntryType::Library => "LIB", + ClasspathEntryType::Project => "PROJ", + ClasspathEntryType::Source => "SRC", + }; + let source = entry.source_attachment_path.as_deref().unwrap_or(""); + let access = if entry.access_rules.is_empty() { + "".to_string() + } else { + entry + .access_rules + .iter() + .map(|r| { + if r.is_accessible { + format!("+{}", r.pattern) + } else { + format!("-{}", r.pattern) + } + }) + .collect::>() + .join(":") + }; + result.push(format!( + "{}|{}|{}|{}|{}|{}", + type_str, entry.path, source, entry.is_test, entry.is_exported, access + )); + } + + result } } @@ -928,7 +975,9 @@ mod tests { graph.set_target_jars( "@@rules_jvm_external~maven~maven//:guava", vec![ResolvedJar { - classpath_path: "/guava.jar".to_string(), + full_jar_path: "/guava.jar".to_string(), + compile_jar_path: None, + runtime_jar_path: None, source_path: None, }], ); @@ -939,7 +988,7 @@ mod tests { let jars = graph.get_target_jars("@maven//:guava"); assert!(jars.is_some()); - assert_eq!(jars.unwrap()[0].classpath_path, "/guava.jar"); + assert_eq!(jars.unwrap()[0].full_jar_path, "/guava.jar"); } #[test] @@ -954,7 +1003,9 @@ mod tests { graph.set_target_jars( "@@rules_jvm_external~maven~maven//:com_google_guava_guava", vec![ResolvedJar { - classpath_path: "/guava-33.4.0-jre.jar".to_string(), + full_jar_path: "/guava-33.4.0-jre.jar".to_string(), + compile_jar_path: None, + runtime_jar_path: None, source_path: None, }], ); @@ -1586,4 +1637,345 @@ mod tests { lib_paths ); } + + // --- output_jars serialization tests --- + + #[test] + fn test_output_jars_included_in_pipe_delimited() { + let cp = ComputedClasspath { + target_label: "//app:app".to_string(), + entries: vec![], + source_roots: Vec::new(), + generated_source_dirs: Vec::new(), + annotation_processors: Vec::new(), + output_jars: vec!["/bazel-bin/app/app.jar".to_string()], + }; + let lines = cp.to_pipe_delimited_entries(); + assert_eq!(lines.len(), 1); + assert_eq!( + lines[0], "LIB|/bazel-bin/app/app.jar||false|false|", + "output_jar should be serialized as a LIB entry" + ); + } + + #[test] + fn test_output_jars_before_dependency_entries() { + let cp = ComputedClasspath { + target_label: "//app:app".to_string(), + entries: vec![ClasspathEntry { + entry_type: ClasspathEntryType::Library, + path: "/guava.jar".to_string(), + source_attachment_path: None, + is_test: false, + is_exported: false, + access_rules: Vec::new(), + visibility: Visibility::default(), + }], + source_roots: Vec::new(), + generated_source_dirs: Vec::new(), + annotation_processors: Vec::new(), + output_jars: vec!["/bazel-bin/app/app.jar".to_string()], + }; + let lines = cp.to_pipe_delimited_entries(); + assert_eq!(lines.len(), 2); + assert!( + lines[0].contains("/bazel-bin/app/app.jar"), + "output_jar should appear BEFORE dependency entries, got: {:?}", + lines + ); + assert!( + lines[1].contains("/guava.jar"), + "dependency entry should appear AFTER output_jar, got: {:?}", + lines + ); + } + + #[test] + fn test_empty_output_jars_no_extra_entries() { + let dep_entry = ClasspathEntry { + entry_type: ClasspathEntryType::Library, + path: "/guava.jar".to_string(), + source_attachment_path: None, + is_test: false, + is_exported: false, + access_rules: Vec::new(), + visibility: Visibility::default(), + }; + let cp = ComputedClasspath { + target_label: "//app:app".to_string(), + entries: vec![dep_entry], + source_roots: Vec::new(), + generated_source_dirs: Vec::new(), + annotation_processors: Vec::new(), + output_jars: Vec::new(), + }; + let lines = cp.to_pipe_delimited_entries(); + assert_eq!( + lines.len(), + 1, + "Empty output_jars should not add extra entries" + ); + } + + #[test] + fn test_merged_targets_output_jars_in_pipe_delimited() { + let mut graph = DependencyGraph::new(); + let results = vec![ + make_target("//pkg:A", vec![], vec!["/a.jar"]), + make_target("//pkg:B", vec![], vec!["/b.jar"]), + ]; + graph.populate_from_aspects(&results, Path::new("/workspace")); + + let cp = + ComputedClasspath::compute_for_targets(&graph, &["//pkg:A", "//pkg:B"], None).unwrap(); + let lines = cp.to_pipe_delimited_entries(); + + let output_jar_lines: Vec<&String> = lines + .iter() + .filter(|l| l.contains("/a.jar") || l.contains("/b.jar")) + .collect(); + assert!( + output_jar_lines.len() >= 2, + "Expected output_jars from both targets in pipe-delimited output, got: {:?}", + lines + ); + } + + #[test] + fn test_java_import_output_jars_serialized() { + let mut graph = DependencyGraph::new(); + let results = vec![make_target_with_jar_path( + "@maven//:guava", + vec![], + "/guava.jar", + )]; + graph.populate_from_aspects(&results, Path::new("/workspace")); + + let cp = + ComputedClasspath::compute_for(&graph, "@maven//:guava", TargetKind::JavaImport, None) + .unwrap(); + + assert!( + !cp.output_jars.is_empty(), + "java_import should have output_jars" + ); + let lines = cp.to_pipe_delimited_entries(); + let has_guava = lines.iter().any(|l| l.contains("/guava.jar")); + assert!( + has_guava, + "java_import output_jars should appear in pipe-delimited output, got: {:?}", + lines + ); + } + + #[test] + fn test_output_jars_deduplicated_against_entries() { + let shared_path = "/workspace/bazel-bin/lib/libfoo.jar"; + let cp = ComputedClasspath { + target_label: "//lib:foo".to_string(), + entries: vec![ClasspathEntry { + entry_type: ClasspathEntryType::Library, + path: shared_path.to_string(), + source_attachment_path: Some("/workspace/lib/src".to_string()), + is_test: false, + is_exported: true, + access_rules: vec![], + visibility: Visibility::Public, + }], + output_jars: vec![shared_path.to_string()], + source_roots: vec![], + generated_source_dirs: vec![], + annotation_processors: vec![], + }; + + let lines = cp.to_pipe_delimited_entries(); + let count = lines.iter().filter(|l| l.contains(shared_path)).count(); + assert_eq!( + count, 1, + "JAR present in both output_jars and entries should appear only once, got: {:?}", + lines + ); + assert!( + lines[0].starts_with("LIB|"), + "the single entry should be the dependency LIB entry (with source attachment)" + ); + assert!( + lines[0].contains("/workspace/lib/src"), + "should preserve source attachment from the dependency entry" + ); + } + + fn make_target_with_kind( + label: &str, + kind: &str, + deps: Vec<&str>, + runtime_deps: Vec<&str>, + jar_paths: Vec<&str>, + ) -> TargetIdeInfo { + let jars: Vec = jar_paths + .iter() + .map(|p| JarInfo { + jar: ArtifactLocation { + absolute_path: Some(p.to_string()), + ..Default::default() + }, + ..Default::default() + }) + .collect(); + + TargetIdeInfo { + label: label.to_string(), + kind: kind.to_string(), + build_file: None, + java_info: Some(JavaIdeInfo { + jars, + ..Default::default() + }), + deps: deps.iter().map(|s| s.to_string()).collect(), + runtime_deps: runtime_deps.iter().map(|s| s.to_string()).collect(), + exports: Vec::new(), + } + } + + #[test] + fn test_java_binary_no_srcs_includes_runtime_deps_jars() { + let mut graph = DependencyGraph::new(); + let results = vec![ + make_target_with_kind( + "//app:app", + "java_binary", + vec![], + vec!["//lib:lib"], + vec!["/workspace/bazel-bin/app/app.jar"], + ), + make_target_with_kind( + "//lib:lib", + "java_library", + vec![], + vec![], + vec!["/workspace/bazel-bin/lib/liblib.jar"], + ), + ]; + + graph.populate_from_aspects(&results, Path::new("/workspace")); + let cp = ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None) + .unwrap(); + + assert!( + cp.output_jars + .contains(&"/workspace/bazel-bin/lib/liblib.jar".to_string()), + "java_binary output_jars should contain runtime_deps jar, got: {:?}", + cp.output_jars + ); + } + + #[test] + fn test_java_binary_with_srcs_includes_dep_jars() { + let mut graph = DependencyGraph::new(); + let results = vec![ + make_target_with_kind( + "//app:app", + "java_binary", + vec!["//lib:lib"], + vec![], + vec!["/workspace/bazel-bin/app/app.jar"], + ), + make_target_with_kind( + "//lib:lib", + "java_library", + vec![], + vec![], + vec!["/workspace/bazel-bin/lib/liblib.jar"], + ), + ]; + + graph.populate_from_aspects(&results, Path::new("/workspace")); + let cp = ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None) + .unwrap(); + + assert!( + cp.output_jars + .contains(&"/workspace/bazel-bin/app/app.jar".to_string()), + "should contain target's own jar, got: {:?}", + cp.output_jars + ); + assert!( + cp.output_jars + .contains(&"/workspace/bazel-bin/lib/liblib.jar".to_string()), + "should contain dep jar, got: {:?}", + cp.output_jars + ); + } + + #[test] + fn test_java_binary_dep_jar_deduplicated_in_serialization() { + let mut graph = DependencyGraph::new(); + let results = vec![ + make_target_with_kind( + "//app:app", + "java_binary", + vec!["//lib:lib"], + vec![], + vec!["/workspace/bazel-bin/app/app.jar"], + ), + make_target_with_kind( + "//lib:lib", + "java_library", + vec![], + vec![], + vec!["/workspace/bazel-bin/lib/liblib.jar"], + ), + ]; + + graph.populate_from_aspects(&results, Path::new("/workspace")); + let cp = ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None) + .unwrap(); + + let lines = cp.to_pipe_delimited_entries(); + let lib_count = lines + .iter() + .filter(|l| l.contains("/workspace/bazel-bin/lib/liblib.jar")) + .count(); + assert_eq!( + lib_count, 1, + "dep jar should appear exactly once in serialized output, got: {:?}", + lines + ); + } + + #[test] + fn test_java_library_output_jars_unchanged() { + let mut graph = DependencyGraph::new(); + let results = vec![ + make_target_with_kind( + "//lib:lib", + "java_library", + vec!["//lib:dep"], + vec![], + vec!["/workspace/bazel-bin/lib/liblib.jar"], + ), + make_target_with_kind( + "//lib:dep", + "java_library", + vec![], + vec![], + vec!["/workspace/bazel-bin/lib/libdep.jar"], + ), + ]; + + graph.populate_from_aspects(&results, Path::new("/workspace")); + let cp = ComputedClasspath::compute_for(&graph, "//lib:lib", TargetKind::JavaLibrary, None) + .unwrap(); + + assert_eq!( + cp.output_jars, + vec!["/workspace/bazel-bin/lib/liblib.jar"], + "java_library output_jars should only contain target's own jars" + ); + assert!( + !cp.output_jars + .contains(&"/workspace/bazel-bin/lib/libdep.jar".to_string()), + "java_library output_jars should NOT contain dep jars" + ); + } } diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs b/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs index 77de75f..c3e663d 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs @@ -5,12 +5,48 @@ use petgraph::Direction; use std::collections::{HashMap, HashSet, VecDeque}; /// A classpath JAR paired with its resolved source attachment. +/// Stores both the full JAR path and an optional compile JAR (ijar) fallback. +/// Use `effective_path()` to get the best available path at query time. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedJar { - pub classpath_path: String, + pub full_jar_path: String, + pub compile_jar_path: Option, + pub runtime_jar_path: Option, pub source_path: Option, } +fn is_ijar_path(path: &str) -> bool { + path.contains("/_ijar/") +} + +impl ResolvedJar { + pub fn effective_path(&self) -> &str { + if std::path::Path::new(&self.full_jar_path).exists() { + &self.full_jar_path + } else if let Some(ref runtime) = self.runtime_jar_path { + if std::path::Path::new(runtime).exists() { + runtime + } else if let Some(ref compile) = self.compile_jar_path { + if is_ijar_path(compile) { + &self.full_jar_path + } else { + compile + } + } else { + &self.full_jar_path + } + } else if let Some(ref compile) = self.compile_jar_path { + if is_ijar_path(compile) { + &self.full_jar_path + } else { + compile + } + } else { + &self.full_jar_path + } + } +} + /// Dependency graph of Bazel targets pub struct DependencyGraph { graph: DiGraph, @@ -228,26 +264,27 @@ impl DependencyGraph { .filter_map(|j| normalize_artifact_path(j, workspace_root)) .collect(); - let jars = if full_jars.is_empty() { - compile_jars + let runtime_jars: Vec = java_info + .runtime_jars + .iter() + .filter_map(|j| normalize_artifact_path(j, workspace_root)) + .collect(); + + let (effective_full, effective_compile) = if full_jars.is_empty() { + (compile_jars, Vec::new()) } else { - full_jars - .into_iter() - .enumerate() - .map(|(i, jar)| { - if std::path::Path::new(&jar).exists() { - jar - } else if i < compile_jars.len() { - compile_jars[i].clone() - } else { - jar - } - }) - .collect() + (full_jars, compile_jars) }; - if !jars.is_empty() { - let resolved = build_resolved_jars(java_info, &jars, workspace_root, label); + if !effective_full.is_empty() { + let resolved = build_resolved_jars( + java_info, + &effective_full, + &effective_compile, + &runtime_jars, + workspace_root, + label, + ); self.set_target_jars(label, resolved); } @@ -614,8 +651,12 @@ fn normalize_artifact_path( workspace_root: &std::path::Path, ) -> Option { let path = artifact.best_path()?; - Some(if artifact.is_source { + Some(if artifact.is_source || path.starts_with("external/") { resolve_external_path(&path, workspace_root).unwrap_or(path) + } else if path.starts_with('/') { + path + } else if path.starts_with("bazel-out/") { + workspace_root.join(&path).to_string_lossy().into_owned() } else { path }) @@ -624,7 +665,9 @@ fn normalize_artifact_path( /// Build `Vec` using a 6-strategy source resolution chain. fn build_resolved_jars( java_info: &bazel_aspect::JavaIdeInfo, - classpath_jars: &[String], + full_jars: &[String], + compile_jars: &[String], + runtime_jars: &[String], workspace_root: &std::path::Path, label: &str, ) -> Vec { @@ -669,21 +712,44 @@ fn build_resolved_jars( }) .collect(); - let single_source = if all_source_jars.len() == 1 && classpath_jars.len() == 1 { + let single_source = if all_source_jars.len() == 1 && full_jars.len() == 1 { all_source_jars.into_iter().next() } else { None }; + let runtime_jar_by_filename: HashMap> = { + let mut counts: HashMap> = HashMap::new(); + for rt_path in runtime_jars { + if let Some(fname) = std::path::Path::new(rt_path).file_name() { + counts + .entry(fname.to_string_lossy().into_owned()) + .or_default() + .push(rt_path.clone()); + } + } + counts + .into_iter() + .map(|(fname, paths)| { + if paths.len() == 1 { + (fname, Some(paths.into_iter().next().unwrap())) + } else { + (fname, None) + } + }) + .collect() + }; + let ws_str = workspace_root.to_str().unwrap_or(""); let mut stubs_discarded: u32 = 0; let mut maven_cache_hits: u32 = 0; let mut with_source: u32 = 0; - let result = classpath_jars + let result = full_jars .iter() - .map(|jar_path| { + .enumerate() + .map(|(i, jar_path)| { let mut strategy = ""; let source_path = class_to_source .get(jar_path) @@ -770,8 +836,18 @@ fn build_resolved_jars( with_source += 1; } + let runtime_jar_path = std::path::Path::new(jar_path) + .file_name() + .and_then(|fname| { + runtime_jar_by_filename + .get(fname.to_string_lossy().as_ref()) + .and_then(|opt| opt.clone()) + }); + ResolvedJar { - classpath_path: jar_path.clone(), + full_jar_path: jar_path.clone(), + compile_jar_path: compile_jars.get(i).cloned(), + runtime_jar_path, source_path, } }) @@ -781,7 +857,7 @@ fn build_resolved_jars( "[bazel-jdt] Source resolution for '{}': {}/{} with source, {} stubs discarded, {} maven cache hits", label, with_source, - classpath_jars.len(), + full_jars.len(), stubs_discarded, maven_cache_hits ); @@ -1083,7 +1159,7 @@ mod tests { assert_eq!(graph.target_count(), 1); let jars = graph.get_target_jars("//foo:lib").unwrap(); - assert_eq!(jars[0].classpath_path, "/second.jar"); + assert_eq!(jars[0].full_jar_path, "/second.jar"); } #[test] @@ -1280,7 +1356,7 @@ mod tests { ); let jar_list = jars.unwrap(); assert_eq!(jar_list.len(), 1); - assert_eq!(jar_list[0].classpath_path, "/guava.jar"); + assert_eq!(jar_list[0].full_jar_path, "/guava.jar"); } #[test] @@ -1320,9 +1396,14 @@ mod tests { let jars = graph.get_target_jars("//lib:mylib").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0].classpath_path, + jars[0].full_jar_path, full_jar_path.to_string_lossy(), - "Full JARs should be preferred over compile_jars when they exist on disk" + "full_jar_path should store the full JAR path" + ); + assert_eq!( + jars[0].compile_jar_path.as_deref(), + Some("/compile.jar"), + "compile_jar_path should store the compile JAR path" ); let _ = std::fs::remove_file(&full_jar_path); } @@ -1365,9 +1446,14 @@ mod tests { let jars = graph.get_target_jars("//thrift:service").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0].classpath_path, + jars[0].full_jar_path, full_jar_path.to_string_lossy(), - "Derived internal full JARs should be preferred over header JARs when they exist" + "full_jar_path should store the full JAR path" + ); + assert_eq!( + jars[0].compile_jar_path.as_deref(), + Some("/libservice-hjar.jar"), + "compile_jar_path should store the header JAR path" ); let _ = std::fs::remove_file(&full_jar_path); } @@ -1403,7 +1489,7 @@ mod tests { let jars = graph.get_target_jars("//lib:plain").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0].classpath_path, "/libplain.jar", + jars[0].full_jar_path, "/libplain.jar", "jars should be kept when compile_jars is empty" ); } @@ -1440,8 +1526,216 @@ mod tests { let jars = graph.get_target_jars("//lib:fallback").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0].classpath_path, "/existing/libfallback-hjar.jar", - "Should fall back to compile_jar (hjar) when full JAR does not exist on disk" + jars[0].full_jar_path, "/nonexistent/libfallback.jar", + "full_jar_path should store the full JAR path even when it does not exist" + ); + assert_eq!( + jars[0].compile_jar_path.as_deref(), + Some("/existing/libfallback-hjar.jar"), + "compile_jar_path should store the hjar path" + ); + assert_eq!( + jars[0].effective_path(), + "/existing/libfallback-hjar.jar", + "effective_path() should fall back to compile_jar when full JAR does not exist" + ); + } + + #[test] + fn test_effective_path_returns_full_jar_when_exists() { + let tmp_dir = std::env::temp_dir(); + let jar_file = tmp_dir.join("test_effective_path_exists.jar"); + std::fs::write(&jar_file, b"fake jar").unwrap(); + + let jar = ResolvedJar { + full_jar_path: jar_file.to_string_lossy().into_owned(), + compile_jar_path: Some("/compile/ijar.jar".to_string()), + runtime_jar_path: None, + source_path: None, + }; + + assert_eq!(jar.effective_path(), jar_file.to_string_lossy().as_ref()); + let _ = std::fs::remove_file(&jar_file); + } + + #[test] + fn test_effective_path_returns_compile_jar_when_full_missing() { + let jar = ResolvedJar { + full_jar_path: "/nonexistent/full.jar".to_string(), + compile_jar_path: Some("/compile/ijar.jar".to_string()), + runtime_jar_path: None, + source_path: None, + }; + + assert_eq!(jar.effective_path(), "/compile/ijar.jar"); + } + + #[test] + fn test_effective_path_returns_full_jar_when_neither_exists() { + let jar = ResolvedJar { + full_jar_path: "/nonexistent/full.jar".to_string(), + compile_jar_path: None, + runtime_jar_path: None, + source_path: None, + }; + + assert_eq!(jar.effective_path(), "/nonexistent/full.jar"); + } + + #[test] + fn test_effective_path_prefers_runtime_jar_over_compile_jar() { + let tmp_dir = std::env::temp_dir(); + let runtime_file = tmp_dir.join("test_runtime_jar_preferred.jar"); + std::fs::write(&runtime_file, b"runtime jar").unwrap(); + + let jar = ResolvedJar { + full_jar_path: "/nonexistent/full.jar".to_string(), + compile_jar_path: Some("/compile/ijar.jar".to_string()), + runtime_jar_path: Some(runtime_file.to_string_lossy().into_owned()), + source_path: None, + }; + + assert_eq!( + jar.effective_path(), + runtime_file.to_string_lossy().as_ref() + ); + let _ = std::fs::remove_file(&runtime_file); + } + + #[test] + fn test_effective_path_falls_to_compile_when_runtime_missing() { + let jar = ResolvedJar { + full_jar_path: "/nonexistent/full.jar".to_string(), + compile_jar_path: Some("/compile/ijar.jar".to_string()), + runtime_jar_path: Some("/nonexistent/runtime.jar".to_string()), + source_path: None, + }; + + assert_eq!(jar.effective_path(), "/compile/ijar.jar"); + } + + #[test] + fn test_effective_path_skips_ijar_compile_jar() { + let jar = ResolvedJar { + full_jar_path: "/nonexistent/libaws_java_sdk_core.jar".to_string(), + compile_jar_path: Some( + "/bazel-out/bin/external/maven/com/amazonaws/aws-java-sdk-core/_ijar/downloaded-ijar.jar" + .to_string(), + ), + runtime_jar_path: None, + source_path: None, + }; + + assert_eq!( + jar.effective_path(), + "/nonexistent/libaws_java_sdk_core.jar", + "effective_path() should skip ijar compile_jar and return full_jar_path" + ); + } + + #[test] + fn test_effective_path_allows_non_ijar_compile_jar() { + let jar = ResolvedJar { + full_jar_path: "/nonexistent/full.jar".to_string(), + compile_jar_path: Some("/compile/normal-header.jar".to_string()), + runtime_jar_path: None, + source_path: None, + }; + + assert_eq!( + jar.effective_path(), + "/compile/normal-header.jar", + "effective_path() should still return non-ijar compile_jar" + ); + } + + #[test] + fn test_effective_path_runtime_jar_unaffected_by_ijar_filter() { + let tmp_dir = std::env::temp_dir(); + let runtime_file = tmp_dir.join("test_runtime_ijar_unaffected.jar"); + std::fs::write(&runtime_file, b"runtime jar").unwrap(); + + let jar = ResolvedJar { + full_jar_path: "/nonexistent/full.jar".to_string(), + compile_jar_path: Some( + "/bazel-out/bin/external/maven/foo/_ijar/downloaded-ijar.jar".to_string(), + ), + runtime_jar_path: Some(runtime_file.to_string_lossy().into_owned()), + source_path: None, + }; + + assert_eq!( + jar.effective_path(), + runtime_file.to_string_lossy().as_ref(), + "effective_path() should prefer runtime_jar even when compile_jar is an ijar" + ); + let _ = std::fs::remove_file(&runtime_file); + } + + #[test] + fn test_effective_path_skips_ijar_when_runtime_missing() { + let jar = ResolvedJar { + full_jar_path: "/nonexistent/full.jar".to_string(), + compile_jar_path: Some( + "/bazel-out/bin/external/maven/foo/_ijar/downloaded-ijar.jar".to_string(), + ), + runtime_jar_path: Some("/nonexistent/runtime.jar".to_string()), + source_path: None, + }; + + assert_eq!( + jar.effective_path(), + "/nonexistent/full.jar", + "effective_path() should skip ijar compile_jar even when runtime_jar is set but missing" + ); + } + + #[test] + fn test_build_resolved_jars_matches_runtime_jar_by_filename() { + let java_info = bazel_aspect::JavaIdeInfo::default(); + let workspace = std::path::PathBuf::from("/workspace"); + let full_jars = vec!["/bazel-out/bin/external/foo/downloaded.jar".to_string()]; + let runtime_jars = vec!["/execroot/external/foo/downloaded.jar".to_string()]; + + let resolved = build_resolved_jars( + &java_info, + &full_jars, + &[], + &runtime_jars, + &workspace, + "//foo:lib", + ); + + assert_eq!(resolved.len(), 1); + assert_eq!( + resolved[0].runtime_jar_path.as_deref(), + Some("/execroot/external/foo/downloaded.jar"), + ); + } + + #[test] + fn test_build_resolved_jars_duplicate_runtime_filename_gives_none() { + let java_info = bazel_aspect::JavaIdeInfo::default(); + let workspace = std::path::PathBuf::from("/workspace"); + let full_jars = vec!["/bazel-out/bin/external/foo/classes.jar".to_string()]; + let runtime_jars = vec![ + "/execroot/external/foo/classes.jar".to_string(), + "/execroot/external/bar/classes.jar".to_string(), + ]; + + let resolved = build_resolved_jars( + &java_info, + &full_jars, + &[], + &runtime_jars, + &workspace, + "//foo:lib", + ); + + assert_eq!(resolved.len(), 1); + assert!( + resolved[0].runtime_jar_path.is_none(), + "Duplicate runtime filenames should result in None to avoid ambiguity" ); } @@ -1524,7 +1818,7 @@ mod tests { let jars = graph.get_target_jars("//lib:utils").unwrap(); let output_jar = jars .iter() - .find(|j| j.classpath_path == output_jar_path.to_string_lossy().as_ref()) + .find(|j| j.full_jar_path == output_jar_path.to_string_lossy().as_ref()) .expect("Expected output.jar"); assert_eq!( output_jar.source_path.as_deref(), @@ -1533,7 +1827,7 @@ mod tests { ); let extra_jar = jars .iter() - .find(|j| j.classpath_path == extra_jar_path.to_string_lossy().as_ref()) + .find(|j| j.full_jar_path == extra_jar_path.to_string_lossy().as_ref()) .expect("Expected extra.jar"); assert_eq!( extra_jar.source_path, None, @@ -1584,7 +1878,7 @@ mod tests { let jars = graph.get_target_jars("@maven//:guava").unwrap(); let jar = jars .iter() - .find(|j| j.classpath_path == bin_path) + .find(|j| j.full_jar_path == bin_path) .expect("Expected compile_jar entry"); assert_eq!( jar.source_path, @@ -1603,7 +1897,9 @@ mod tests { graph.set_target_jars( canonical, vec![ResolvedJar { - classpath_path: "/guava.jar".to_string(), + full_jar_path: "/guava.jar".to_string(), + compile_jar_path: None, + runtime_jar_path: None, source_path: Some("/guava-sources.jar".to_string()), }], ); @@ -1752,7 +2048,7 @@ mod tests { "JAR data should be preserved after deps-only change" ); assert_eq!( - graph.get_target_jars("//foo:lib").unwrap()[0].classpath_path, + graph.get_target_jars("//foo:lib").unwrap()[0].full_jar_path, "/foo.jar" ); @@ -1801,7 +2097,9 @@ mod tests { graph.set_target_jars( "//foo:old", vec![ResolvedJar { - classpath_path: "/old.jar".to_string(), + full_jar_path: "/old.jar".to_string(), + compile_jar_path: None, + runtime_jar_path: None, source_path: None, }], ); @@ -2106,10 +2404,10 @@ mod tests { assert_eq!(jars.len(), 1); assert!( jars[0] - .classpath_path + .full_jar_path .contains("/execroot/external/org_example/jar/downloaded.jar"), "Expected JAR resolved to execroot, got: {}", - jars[0].classpath_path + jars[0].full_jar_path ); } @@ -2145,7 +2443,7 @@ mod tests { let jars = graph.get_target_jars("//lib:foo").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0].classpath_path, jar_path, + jars[0].full_jar_path, jar_path, "Non-external JAR path should remain unchanged" ); } @@ -2186,7 +2484,7 @@ mod tests { let jars = graph.get_target_jars("//app:app").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0].classpath_path, stale_path, + jars[0].full_jar_path, stale_path, "Original path should be preserved when resolve_external_path returns None" ); } @@ -2234,15 +2532,15 @@ mod tests { let jars = graph.get_target_jars("//app:app").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0].classpath_path, derived_path, + jars[0].full_jar_path, derived_path, "Derived artifact path should be preserved without resolve_external_path mangling" ); assert!( jars[0] - .classpath_path + .full_jar_path .contains("bazel-out/k8-fastbuild/bin/external/maven"), "bazel-out//bin/ prefix must be retained, got: {}", - jars[0].classpath_path + jars[0].full_jar_path ); } @@ -2300,7 +2598,7 @@ mod tests { let jars = graph.get_target_jars("//app:app").unwrap(); let jar = jars .iter() - .find(|j| j.classpath_path == bin_path) + .find(|j| j.full_jar_path == bin_path) .expect("Expected bin jar"); let resolved_src = jar.source_path.as_ref().unwrap(); assert_eq!( @@ -2427,7 +2725,14 @@ mod tests { }; let classpath_jars = vec![classpath_jar.to_string_lossy().into_owned()]; - let resolved = build_resolved_jars(&java_info, &classpath_jars, &workspace, "//lib:lib"); + let resolved = build_resolved_jars( + &java_info, + &classpath_jars, + &[], + &[], + &workspace, + "//lib:lib", + ); assert_eq!(resolved.len(), 1); assert!( @@ -2467,7 +2772,14 @@ mod tests { }; let classpath_jars = vec![classpath_jar.to_string_lossy().into_owned()]; - let resolved = build_resolved_jars(&java_info, &classpath_jars, &workspace, "//lib:lib"); + let resolved = build_resolved_jars( + &java_info, + &classpath_jars, + &[], + &[], + &workspace, + "//lib:lib", + ); assert_eq!(resolved.len(), 1); assert_eq!( @@ -2505,7 +2817,14 @@ mod tests { }; let classpath_jars = vec![classpath_jar.to_string_lossy().into_owned()]; - let resolved = build_resolved_jars(&java_info, &classpath_jars, &workspace, "//lib:lib"); + let resolved = build_resolved_jars( + &java_info, + &classpath_jars, + &[], + &[], + &workspace, + "//lib:lib", + ); assert_eq!(resolved.len(), 1); assert!( @@ -2581,8 +2900,14 @@ mod tests { std::env::set_var("HOME", tmp.path()); let classpath_jars = vec![classpath_jar_path.to_string_lossy().into_owned()]; - let resolved = - build_resolved_jars(&java_info, &classpath_jars, &workspace, "@maven//:guava"); + let resolved = build_resolved_jars( + &java_info, + &classpath_jars, + &[], + &[], + &workspace, + "@maven//:guava", + ); assert_eq!(resolved.len(), 1); assert_eq!( @@ -2623,7 +2948,14 @@ mod tests { std::env::set_var("HOME", tmp.path()); let classpath_jars = vec![classpath_jar.to_string_lossy().into_owned()]; - let resolved = build_resolved_jars(&java_info, &classpath_jars, &workspace, "//lib:lib"); + let resolved = build_resolved_jars( + &java_info, + &classpath_jars, + &[], + &[], + &workspace, + "//lib:lib", + ); assert_eq!(resolved.len(), 1); assert!( @@ -2661,8 +2993,14 @@ mod tests { std::env::set_var("HOME", tmp.path()); let classpath_jars = vec![classpath_jar.to_string_lossy().into_owned()]; - let resolved = - build_resolved_jars(&java_info, &classpath_jars, &workspace, "@maven//:guava"); + let resolved = build_resolved_jars( + &java_info, + &classpath_jars, + &[], + &[], + &workspace, + "@maven//:guava", + ); assert_eq!(resolved.len(), 1); assert_eq!( @@ -2795,4 +3133,94 @@ mod tests { graph.clear(); assert_eq!(graph.get_target_kind("//foo:lib"), TargetKind::Unknown); } + + #[test] + fn test_normalize_artifact_path_resolves_external_when_not_source() { + let artifact = ArtifactLocation { + relative_path: Some("external/maven/v1/com/amazonaws/aws-java-sdk-core/1.12.261/aws-java-sdk-core-1.12.261.jar".to_string()), + is_source: false, + is_external: true, + ..Default::default() + }; + let tmp = tempfile::tempdir().unwrap(); + let result = normalize_artifact_path(&artifact, tmp.path()); + assert!(result.is_some()); + let path = result.unwrap(); + assert!( + path != "external/maven/v1/com/amazonaws/aws-java-sdk-core/1.12.261/aws-java-sdk-core-1.12.261.jar" + || !tmp.path().join("bazel-out").exists(), + "When bazel-out exists, external path should be resolved; got: {}", + path + ); + } + + #[test] + fn test_normalize_artifact_path_absolutizes_bazel_out_paths() { + let artifact = ArtifactLocation { + relative_path: Some("bazel-out/k8-fastbuild/bin/app/libapp.jar".to_string()), + is_source: false, + is_external: false, + ..Default::default() + }; + let result = normalize_artifact_path(&artifact, Path::new("/workspace")); + assert_eq!( + result, + Some("/workspace/bazel-out/k8-fastbuild/bin/app/libapp.jar".to_string()), + "bazel-out/ paths should be absolutized with workspace_root" + ); + } + + #[test] + fn test_populate_from_aspects_resolves_external_full_jar_paths() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path(); + + let external_jar = "external/maven/v1/com/example/lib/1.0/lib-1.0.jar"; + let compile_jar = + "bazel-out/k8-fastbuild/bin/external/maven/v1/com/example/lib/1.0/header-lib-1.0.jar"; + + let target = TargetIdeInfo { + label: "//app:app".to_string(), + kind: "java_library".to_string(), + build_file: None, + java_info: Some(JavaIdeInfo { + jars: vec![JarInfo { + jar: ArtifactLocation { + relative_path: Some(external_jar.to_string()), + is_source: false, + is_external: true, + ..Default::default() + }, + interface_jar: Some(ArtifactLocation { + relative_path: Some(compile_jar.to_string()), + is_source: false, + is_external: false, + ..Default::default() + }), + source_jar: None, + }], + ..Default::default() + }), + deps: vec![], + runtime_deps: vec![], + exports: vec![], + }; + + let mut graph = DependencyGraph::new(); + graph.populate_from_aspects(&[target], workspace); + + let jars = graph + .get_target_jars("//app:app") + .expect("should have jars for //app:app"); + assert!( + !jars.is_empty(), + "Should have at least one jar for //app:app" + ); + let jar = &jars[0]; + assert!( + jar.full_jar_path != external_jar || !workspace.join("bazel-out").exists(), + "External full_jar_path should be resolved when bazel-out symlink exists; got: {}", + jar.full_jar_path + ); + } } diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/src/aspects/artifacts.bzl b/bazel-jdt-bridge/crates/bazel-jdt-core/src/aspects/artifacts.bzl index d665488..c82119a 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/src/aspects/artifacts.bzl +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/src/aspects/artifacts.bzl @@ -30,16 +30,18 @@ def artifact_location(f): return to_artifact_location( root_path = root_path, relative_path = relative_path, + absolute_path = f.path, is_source = f.is_source, is_external = is_external_artifact(f.owner), ) -def to_artifact_location(root_path, relative_path, is_source, is_external): +def to_artifact_location(root_path, relative_path, is_source, is_external, absolute_path = None): """Creates creates an ArtifactLocation proto.""" return struct_omit_none( relative_path = relative_path, root_path = root_path, + absolute_path = absolute_path, is_source = is_source, is_external = is_external, ) diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs b/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs index f823575..2602ca8 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs @@ -6,7 +6,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; use jni::objects::{JClass, JObject, JObjectArray, JString}; -use jni::sys::{jint, jlong, jobjectArray, jsize}; +use jni::sys::{jboolean, jint, jlong, jobjectArray, jsize}; use jni::JNIEnv; static REGISTRY: OnceLock>>> = OnceLock::new(); @@ -496,6 +496,45 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeRunAspectBuild( } } +#[no_mangle] +pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeBuildTargets( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + targets: JObjectArray, + build_flags: JObjectArray, +) -> jboolean { + let state = match get_state(&mut env, handle) { + Some(s) => s, + None => return 0, + }; + + let target_vec = match parse_java_string_array(&mut env, &targets) { + Some(t) => t, + None => return 0, + }; + let build_flags_vec = parse_java_string_array(&mut env, &build_flags); + let build_flags_ref: Option<&[String]> = build_flags_vec.as_deref(); + + log::info!( + "nativeBuildTargets: plain build for {} targets", + target_vec.len() + ); + match state + .invoker + .build_targets_sync(&target_vec, build_flags_ref) + { + Ok(()) => { + log::info!("Plain build complete"); + 1 + } + Err(e) => { + log::warn!("Plain build failed: {}", e); + 0 + } + } +} + #[no_mangle] pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeComputeClasspath( mut env: JNIEnv, diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/tests/integration.rs b/bazel-jdt-bridge/crates/bazel-jdt-core/tests/integration.rs index 95165f4..28b9135 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/tests/integration.rs +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/tests/integration.rs @@ -66,21 +66,27 @@ fn dependency_chain_classpath() { graph.set_target_jars( "//utils:utils", vec![ResolvedJar { - classpath_path: "utils.jar".to_string(), + full_jar_path: "utils.jar".to_string(), + compile_jar_path: None, + runtime_jar_path: None, source_path: None, }], ); graph.set_target_jars( "//service:service", vec![ResolvedJar { - classpath_path: "service.jar".to_string(), + full_jar_path: "service.jar".to_string(), + compile_jar_path: None, + runtime_jar_path: None, source_path: None, }], ); graph.set_target_jars( "//app:app", vec![ResolvedJar { - classpath_path: "app.jar".to_string(), + full_jar_path: "app.jar".to_string(), + compile_jar_path: None, + runtime_jar_path: None, source_path: None, }], ); @@ -192,14 +198,18 @@ fn pipe_delimited_classpath_output() { graph.set_target_jars( "//lib:lib", vec![ResolvedJar { - classpath_path: "lib.jar".to_string(), + full_jar_path: "lib.jar".to_string(), + compile_jar_path: None, + runtime_jar_path: None, source_path: None, }], ); graph.set_target_jars( "//app:app", vec![ResolvedJar { - classpath_path: "app.jar".to_string(), + full_jar_path: "app.jar".to_string(), + compile_jar_path: None, + runtime_jar_path: None, source_path: None, }], ); diff --git a/bazel-jdt-bridge/crates/bazel-query/src/command.rs b/bazel-jdt-bridge/crates/bazel-query/src/command.rs index 98049b3..0112c13 100644 --- a/bazel-jdt-bridge/crates/bazel-query/src/command.rs +++ b/bazel-jdt-bridge/crates/bazel-query/src/command.rs @@ -172,6 +172,35 @@ impl BazelInvoker { Ok(stderr) } + /// Synchronous plain build — compiles targets without aspects or graph updates. + pub fn build_targets_sync( + &self, + targets: &[String], + build_flags: Option<&[String]>, + ) -> Result<(), BazelError> { + if targets.is_empty() { + return Ok(()); + } + + let mut args = vec!["build".to_string()]; + if let Some(flags) = build_flags { + args.extend(flags.iter().map(|s| s.to_string())); + } + args.extend(targets.iter().cloned()); + + log::info!("Plain build for {} targets", targets.len()); + let output = run_bazel_command_sync(&self.bazel_path, &self.workspace_root, &args)?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(BazelError::CommandFailed { + message: format!("bazel build failed: {}", stderr), + }); + } + + Ok(()) + } + /// Synchronous full classpath resolution via aspect build. pub fn resolve_full_classpath_sync( &self, diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java index fdbc39f..445bd39 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java @@ -18,6 +18,7 @@ public final class BazelBridge { private volatile String dependencySourceLoadingMode = "full-project"; private volatile String syncMode = "fast"; private volatile String[] cachedDependencyPackages = new String[0]; + private volatile BazelProjectView projectView; private static ExecutorService createExecutor() { return Executors.newSingleThreadExecutor(r -> { @@ -57,6 +58,7 @@ public void initialize(String workspacePath, String bazelPath, String cacheDir) lastWorkspacePath = workspacePath; lastBazelPath = bazelPath; lastCacheDir = cacheDir; + projectView = null; } finally { rwLock.writeLock().unlock(); } @@ -146,6 +148,23 @@ public String[] runAspectBuild(String[] targets, String[] buildFlags) { } } + public boolean buildTargets(String[] targets, String[] buildFlags) { + long h = snapshotHandle(); + try { + return jniExecutor.submit(() -> nativeBuildTargets(h, targets, buildFlags)) + .get(DISCOVER_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted during buildTargets", e); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) throw (RuntimeException) cause; + throw new RuntimeException("buildTargets failed", cause); + } catch (TimeoutException e) { + throw new RuntimeException("buildTargets timed out", e); + } + } + public String[] computeClasspath(String targetLabel) { long h = snapshotHandle(); try { @@ -234,6 +253,22 @@ public String[] getCachedDependencyPackages() { return this.cachedDependencyPackages; } + public void setProjectView(BazelProjectView view) { + this.projectView = view; + } + + public BazelProjectView getProjectView() { + return this.projectView; + } + + public String[] getBuildFlags() { + BazelProjectView view = this.projectView; + if (view == null || view.getBuildFlags().isEmpty()) { + return null; + } + return view.getBuildFlags().toArray(new String[0]); + } + public void cleanCache() { long h = snapshotHandle(); try { @@ -337,6 +372,7 @@ private long snapshotHandleNullable() { private native String[] nativeQueryTargets(long handle, String[] scopePatterns); private native void nativePopulateGraph(long handle); private native String[] nativeRunAspectBuild(long handle, String[] targets, String[] buildFlags, String syncMode); + private native boolean nativeBuildTargets(long handle, String[] targets, String[] buildFlags); private native String[] nativeComputeClasspath(long handle, String targetLabel, String[] buildFlags); private native String[] nativeComputeClasspathMerged(long handle, String[] labels); private native int nativeGetSyncState(long handle); diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathManager.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathManager.java index fd0bf67..cfda34f 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathManager.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathManager.java @@ -24,6 +24,10 @@ public class BazelClasspathManager { private static final int BATCH_SIZE = 50; public static void setMergedClasspathContainer(IProject project) { + setMergedClasspathContainer(project, false); + } + + public static void setMergedClasspathContainer(IProject project, boolean force) { try { BazelBridge bridge = BazelBridge.getInstance(); if (!bridge.isInitialized()) { @@ -46,11 +50,13 @@ public static void setMergedClasspathContainer(IProject project) { String[] labels = targetLabels.toArray(new String[0]); String[] rawEntries = bridge.computeClasspathMerged(labels); - String[] cachedEntries = TargetProjectMapping.readCachedClasspath(project, targetLabels.get(0)); - if (cachedEntries != null && Arrays.equals(rawEntries, cachedEntries)) { - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Classpath unchanged for project " + project.getName() + ", skipping container update")); - return; + if (!force) { + String[] cachedEntries = TargetProjectMapping.readCachedClasspath(project, targetLabels.get(0)); + if (cachedEntries != null && Arrays.equals(rawEntries, cachedEntries)) { + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Classpath unchanged for project " + project.getName() + ", skipping container update")); + return; + } } BazelClasspathContainer container = new BazelClasspathContainer( @@ -124,6 +130,7 @@ rawEntries, getTestSourcePatterns(project), */ public static void refreshClasspath() { BazelClasspathContainer.resetWarnings(); + BazelRuntimeClasspathEntryResolver.clearCache(); try { org.eclipse.core.resources.IWorkspace workspace = org.eclipse.core.resources.ResourcesPlugin.getWorkspace(); diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java index ea01eab..729cb2d 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java @@ -3,6 +3,7 @@ import java.util.List; import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; @@ -34,6 +35,10 @@ public Object executeCommand(String commandId, List arguments, IProgress return handleCreateProjectForPackage(arguments, monitor); case "bazel-jdt.waitForIndexesReady": return handleWaitForIndexesReady(); + case "bazel-jdt.buildTarget": + return handleBuildTarget(arguments); + case "bazel-jdt.setActiveDebugProject": + return handleSetActiveDebugProject(arguments); default: return null; } @@ -59,13 +64,9 @@ private Object handleImportProject(List arguments) { } } - String[] buildFlags = null; - if (arguments.size() > 4 && arguments.get(4) instanceof List) { - @SuppressWarnings("unchecked") - List flags = (List) arguments.get(4); - if (!flags.isEmpty()) { - buildFlags = flags.toArray(new String[0]); - } + BazelProjectView projectView = BazelProjectView.parse(new java.io.File(workspacePath)); + if (projectView != null) { + bridge.setProjectView(projectView); } if (arguments.size() > 5 && arguments.get(5) instanceof String) { @@ -89,7 +90,7 @@ private Object handleImportProject(List arguments) { "Sync mode set to: " + syncMode)); } - String[] targets = bridge.discoverTargets(scopePatterns, buildFlags); + String[] targets = bridge.discoverTargets(scopePatterns, bridge.getBuildFlags()); BazelClasspathManager.refreshClasspath(); return null; } catch (Exception e) { @@ -169,6 +170,62 @@ private Object handleWaitForIndexesReady() { } } + private Object handleBuildTarget(List arguments) { + try { + if (arguments.isEmpty() || !(arguments.get(0) instanceof String)) { + throw new IllegalArgumentException("Project name required"); + } + String projectName = (String) arguments.get(0); + + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); + if (!project.exists()) { + throw new IllegalArgumentException("Project not found: " + projectName); + } + + List targets = TargetProjectMapping.readTargets(project); + if (targets.isEmpty()) { + throw new IllegalStateException("No Bazel targets for project: " + projectName); + } + + BazelBridge bridge = BazelBridge.getInstance(); + if (!bridge.isInitialized()) { + throw new IllegalStateException( + "Bazel project not imported yet. Import the project first."); + } + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Pre-debug build for " + projectName + ": " + targets)); + + boolean buildSuccess = bridge.buildTargets( + targets.toArray(new String[0]), bridge.getBuildFlags()); + if (!buildSuccess) { + String msg = "Bazel build failed for targets: " + targets + + " (project: " + projectName + ")"; + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", msg)); + throw new RuntimeException(msg); + } + + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Pre-debug build complete for " + projectName)); + + BazelRuntimeClasspathEntryResolver.clearCacheForProject(projectName); + BazelClasspathContainer.resetWarnings(); + BazelClasspathManager.setMergedClasspathContainer(project, false); + + return null; + } catch (Exception e) { + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", + "Pre-debug build failed", e)); + throw new RuntimeException("Pre-debug build failed: " + e.getMessage(), e); + } + } + + private Object handleSetActiveDebugProject(List arguments) { + if (!arguments.isEmpty() && arguments.get(0) instanceof String) { + BazelRuntimeClasspathEntryResolver.setActiveDebugProject((String) arguments.get(0)); + } + return null; + } + private Object handleCreateProjectForPackage(List arguments, IProgressMonitor monitor) { try { if (arguments.isEmpty() || !(arguments.get(0) instanceof String)) { diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java index 3289ab1..5cff04a 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java @@ -81,9 +81,8 @@ public void importToWorkspace(IProgressMonitor monitor) throws CoreException { "Scoped import with " + patterns.size() + " patterns from .bazelproject")); } - String[] buildFlags = null; - if (projectView != null && !projectView.getBuildFlags().isEmpty()) { - buildFlags = projectView.getBuildFlags().toArray(new String[0]); + if (projectView != null) { + bridge.setProjectView(projectView); } LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", @@ -136,7 +135,7 @@ public void importToWorkspace(IProgressMonitor monitor) throws CoreException { long phaseStart = System.currentTimeMillis(); LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", "Phase 3/3: running aspect build for " + targets.length + " targets...")); - finalTargets = bridge.runAspectBuild(targets, buildFlags); + finalTargets = bridge.runAspectBuild(targets, bridge.getBuildFlags()); long phaseElapsed = (System.currentTimeMillis() - phaseStart) / 1000; String aspectStats = bridge.getAspectBuildStats(); @@ -256,11 +255,10 @@ private boolean tryFastReload(IProgressMonitor monitor) throws CoreException { String bazelPath = config[1]; String cacheDir = config[2]; - if (rootFolder != null) { - BazelProjectView projectView = BazelProjectView.parse(rootFolder); - if (projectView != null && !projectView.getBazelBinary().isEmpty()) { - bazelPath = projectView.getBazelBinary(); - } + BazelProjectView projectView = rootFolder != null + ? BazelProjectView.parse(rootFolder) : null; + if (projectView != null && !projectView.getBazelBinary().isEmpty()) { + bazelPath = projectView.getBazelBinary(); } long startTime = System.currentTimeMillis(); @@ -270,14 +268,15 @@ private boolean tryFastReload(IProgressMonitor monitor) throws CoreException { BazelBridge bridge = BazelBridge.getInstance(); bridge.initialize(workspacePath, bazelPath, cacheDir); + if (projectView != null) { + bridge.setProjectView(projectView); + } + ensureBazelProjectsGitignore(workspacePath); - if (rootFolder != null) { - BazelProjectView projectView = BazelProjectView.parse(rootFolder); - if (projectView != null && !projectView.getDirectories().isEmpty()) { - String[] watchDirs = projectView.getDirectories().toArray(new String[0]); - bridge.updateWatchPaths(watchDirs); - } + if (projectView != null && !projectView.getDirectories().isEmpty()) { + String[] watchDirs = projectView.getDirectories().toArray(new String[0]); + bridge.updateWatchPaths(watchDirs); } final String wsPath = workspacePath; diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java new file mode 100644 index 0000000..24bc16f --- /dev/null +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java @@ -0,0 +1,118 @@ +package com.bazel.jdt; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.launching.IRuntimeClasspathEntry; +import org.eclipse.jdt.launching.IRuntimeClasspathEntryResolver; +import org.eclipse.jdt.launching.IVMInstall; +import org.eclipse.jdt.launching.JavaRuntime; + +public class BazelRuntimeClasspathEntryResolver implements IRuntimeClasspathEntryResolver { + private static final ILog LOG = Platform.getLog(BazelRuntimeClasspathEntryResolver.class); + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + private static final IRuntimeClasspathEntry[] EMPTY = new IRuntimeClasspathEntry[0]; + private static volatile String activeDebugProject; + + @Override + public IRuntimeClasspathEntry[] resolveRuntimeClasspathEntry( + IRuntimeClasspathEntry entry, ILaunchConfiguration configuration) throws CoreException { + IJavaProject project = entry.getJavaProject(); + if (project == null) { + return EMPTY; + } + return resolve(project); + } + + @Override + public IRuntimeClasspathEntry[] resolveRuntimeClasspathEntry( + IRuntimeClasspathEntry entry, IJavaProject project) throws CoreException { + if (project == null) { + return EMPTY; + } + return resolve(project); + } + + @Override + public IVMInstall resolveVMInstall(IClasspathEntry entry) throws CoreException { + return null; + } + + private IRuntimeClasspathEntry[] resolve(IJavaProject project) { + String projectName = project.getElementName(); + + String active = activeDebugProject; + if (active != null && !active.equals(projectName)) { + return EMPTY; + } + + IRuntimeClasspathEntry[] cached = CACHE.get(projectName); + if (cached != null) { + return cached; + } + + IRuntimeClasspathEntry[] resolved = buildEntries(project); + CACHE.put(projectName, resolved); + return resolved; + } + + private IRuntimeClasspathEntry[] buildEntries(IJavaProject project) { + try { + IClasspathContainer container = JavaCore.getClasspathContainer( + BazelClasspathContainer.CONTAINER_PATH, project); + if (container == null) { + return EMPTY; + } + + List result = new ArrayList<>(); + for (IClasspathEntry cpEntry : container.getClasspathEntries()) { + if (cpEntry.getEntryKind() != IClasspathEntry.CPE_LIBRARY) { + continue; + } + IRuntimeClasspathEntry rte = JavaRuntime.newArchiveRuntimeClasspathEntry(cpEntry.getPath()); + if (cpEntry.getSourceAttachmentPath() != null) { + rte.setSourceAttachmentPath(cpEntry.getSourceAttachmentPath()); + } + if (cpEntry.getSourceAttachmentRootPath() != null) { + rte.setSourceAttachmentRootPath(cpEntry.getSourceAttachmentRootPath()); + } + result.add(rte); + } + + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Resolved " + result.size() + " runtime classpath entries for " + project.getElementName())); + return result.toArray(EMPTY); + } catch (Exception e) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Failed to resolve runtime classpath for " + project.getElementName(), e)); + return EMPTY; + } + } + + public static void clearCache() { + CACHE.clear(); + } + + public static void clearCacheForProject(String projectName) { + CACHE.remove(projectName); + } + + public static void setActiveDebugProject(String projectName) { + activeDebugProject = projectName; + } + + public static void clearActiveDebugProject() { + activeDebugProject = null; + } +} diff --git a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml index e890619..59de6c0 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml +++ b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml @@ -24,9 +24,18 @@ + + + + + + diff --git a/bazel-jdt-bridge/scripts/capture-jdtls-threadump.sh b/bazel-jdt-bridge/scripts/capture-jdtls-threadump.sh new file mode 100755 index 0000000..b656593 --- /dev/null +++ b/bazel-jdt-bridge/scripts/capture-jdtls-threadump.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# +# Captures thread dumps from ALL Java processes at regular intervals +# for diagnosing stackTrace performance issues during Java debugging. +# +# Usage: +# ./scripts/capture-jdtls-threadump.sh [interval_seconds] [max_captures] +# +# Defaults: 10-second interval, 120 captures (20 minutes total) +# Output: logs/threadumps/--.txt + +set -euo pipefail + +INTERVAL="${1:-10}" +MAX_CAPTURES="${2:-120}" +OUTDIR="$(cd "$(dirname "$0")/.." && pwd)/logs/threadumps" +mkdir -p "$OUTDIR" + +echo "=== Java Process Thread Dump Collector ===" +echo "Output directory: $OUTDIR" +echo "Interval: ${INTERVAL}s, Max captures: $MAX_CAPTURES" +echo "Total monitoring time: $(( INTERVAL * MAX_CAPTURES / 60 )) minutes" +echo "" +echo "Will dump ALL Java processes each interval." +echo "Press Ctrl+C to stop early." +echo "---" + +for i in $(seq 1 "$MAX_CAPTURES"); do + TIMESTAMP=$(date '+%Y%m%d_%H%M%S') + + # Find all Java PIDs + PIDS=$(pgrep -f 'java' 2>/dev/null || true) + if [ -z "$PIDS" ]; then + echo "[$TIMESTAMP] Capture $i/$MAX_CAPTURES — no Java processes found, waiting..." + sleep "$INTERVAL" + continue + fi + + PID_COUNT=$(echo "$PIDS" | wc -l | tr -d ' ') + echo "[$TIMESTAMP] Capture $i/$MAX_CAPTURES — found $PID_COUNT Java process(es)" + + for PID in $PIDS; do + # Get a short process label from command line + CMDLINE=$(ps -p "$PID" -o args= 2>/dev/null | head -1 || echo "unknown") + + if echo "$CMDLINE" | grep -q 'equinox.launcher.*jdt'; then + LABEL="jdtls" + elif echo "$CMDLINE" | grep -q 'java.debug'; then + LABEL="debugadapter" + elif echo "$CMDLINE" | grep -q 'DemoApp\|urbancompass'; then + LABEL="debuggee" + else + # Use the main class or jar name + LABEL=$(echo "$CMDLINE" | grep -oE '[^ /]+\.jar|[^ /]+\.[A-Z][a-z]+' | tail -1 | sed 's/\.jar//' || echo "java") + LABEL="${LABEL:-java}" + fi + + OUTFILE="$OUTDIR/${LABEL}-${PID}-${TIMESTAMP}.txt" + if jstack "$PID" > "$OUTFILE" 2>&1; then + THREAD_COUNT=$(grep -c '^"' "$OUTFILE" 2>/dev/null || echo "?") + echo " PID=$PID ($LABEL): $THREAD_COUNT threads → $(basename "$OUTFILE")" + else + echo " PID=$PID ($LABEL): jstack failed (may need sudo or process exited)" + rm -f "$OUTFILE" + fi + done + + if [ "$i" -lt "$MAX_CAPTURES" ]; then + sleep "$INTERVAL" + fi +done + +echo "" +echo "Done. Thread dumps saved to: $OUTDIR" +echo "" +echo "Quick analysis:" +echo " # List all captured processes:" +echo " ls $OUTDIR/ | sed 's/-[0-9]*-[0-9_]*.txt//' | sort -u" +echo "" +echo " # Find threads with debug/stackTrace activity:" +echo " grep -rl 'java.debug\|StackTrace\|stackTrace\|resolveSource' $OUTDIR/" +echo "" +echo " # Find BLOCKED threads:" +echo " grep -rl 'BLOCKED\|waiting to lock' $OUTDIR/" +echo "" +echo " # Find threads in Bazel classpath code:" +echo " grep -rl 'com.bazel.jdt' $OUTDIR/" diff --git a/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts b/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts new file mode 100644 index 0000000..c1c64a9 --- /dev/null +++ b/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts @@ -0,0 +1,73 @@ +import * as vscode from 'vscode'; + +export class BazelDebugConfigurationProvider implements vscode.DebugConfigurationProvider { + async resolveDebugConfiguration( + _folder: vscode.WorkspaceFolder | undefined, + config: vscode.DebugConfiguration, + _token?: vscode.CancellationToken + ): Promise { + const projectName = config.projectName as string | undefined; + if (projectName) { + try { + await vscode.commands.executeCommand( + 'java.execute.workspaceCommand', + 'bazel-jdt.setActiveDebugProject', + projectName + ); + } catch { + // Best-effort: filter not set, debug may be slower but still works + } + } + return config; + } + + async resolveDebugConfigurationWithSubstitutedVariables( + _folder: vscode.WorkspaceFolder | undefined, + config: vscode.DebugConfiguration, + token?: vscode.CancellationToken + ): Promise { + const projectName = config.projectName as string | undefined; + if (!projectName) { + return config; + } + + if (token?.isCancellationRequested) { + return undefined; + } + + try { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `Bazel: building ${projectName}...`, + cancellable: true + }, + (_progress, progressToken) => { + const buildPromise = vscode.commands.executeCommand( + 'java.execute.workspaceCommand', + 'bazel-jdt.buildTarget', + projectName + ); + return new Promise((resolve, reject) => { + progressToken.onCancellationRequested(() => + reject(new Error('Build cancelled by user'))); + token?.onCancellationRequested(() => + reject(new Error('Debug launch cancelled'))); + buildPromise.then(() => resolve(), reject); + }); + } + ); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + const action = await vscode.window.showWarningMessage( + `Bazel build failed: ${msg}`, + 'Debug Anyway', 'Cancel' + ); + if (action !== 'Debug Anyway') { + return undefined; + } + } + + return config; + } +} diff --git a/bazel-jdt-bridge/vscode-extension/src/extension.ts b/bazel-jdt-bridge/vscode-extension/src/extension.ts index b42022c..14237c3 100644 --- a/bazel-jdt-bridge/vscode-extension/src/extension.ts +++ b/bazel-jdt-bridge/vscode-extension/src/extension.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import { registerImportCommand, registerRuntimeCommands } from './commands'; +import { BazelDebugConfigurationProvider } from './debugAdapter'; import { createStatusBar } from './statusBar'; import { getConfig } from './config'; import { parseBazelprojectFile, resolveScopePatterns } from './bazelproject'; @@ -33,6 +34,11 @@ function activateFull(context: vscode.ExtensionContext, workspaceRoot: string) { const statusBarItem = createStatusBar(context); registerImportCommand(context); registerRuntimeCommands(context); + context.subscriptions.push( + vscode.debug.registerDebugConfigurationProvider( + 'java', new BazelDebugConfigurationProvider() + ) + ); let dependencyPackageCache: string[] = [];