From 67389bce3cb1301fe5e9ef30b3a9ed3ab40f97f6 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Thu, 21 May 2026 17:55:22 +0800 Subject: [PATCH 1/9] feat: include output_jars in JNI classpath serialization for Run/Debug support When JavaBuilder is disabled (Bazel is the build system), the Eclipse output location is empty. This causes ClassNotFoundException at runtime because the target's own compiled JARs were computed but never serialized across the JNI boundary. Include output_jars as LIB entries in to_pipe_delimited_entries(), deduplicated against existing dependency entries to avoid duplicates from java_import targets. Co-Authored-By: Claude Opus 4.6 --- .../crates/bazel-graph/src/classpath.rs | 247 +++++++++++++++--- 1 file changed, 215 insertions(+), 32 deletions(-) diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs index 0bb5443..904aff5 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs @@ -327,39 +327,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 } } @@ -1586,4 +1598,175 @@ 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" + ); + } } From cb65fb2c2bf96e8a168a8dd5d9642e48f69703eb Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Thu, 21 May 2026 18:03:02 +0800 Subject: [PATCH 2/9] feat: auto-build Bazel targets before Run/Debug launches Register a DebugConfigurationProvider that intercepts Java debug sessions and runs `bazel build` for the project's targets before the debugger starts. This ensures compiled JARs exist in bazel-bin/ so the runtime classpath (populated by Phase 1's output_jars serialization) resolves successfully. On build failure the user can choose "Debug Anyway" or cancel the launch. The build progress is shown as a cancellable notification. Co-Authored-By: Claude Opus 4.6 --- .../com/bazel/jdt/BazelCommandHandler.java | 40 ++++++++++++++ .../java-bridge/src/main/resources/plugin.xml | 1 + .../vscode-extension/src/debugAdapter.ts | 53 +++++++++++++++++++ .../vscode-extension/src/extension.ts | 6 +++ 4 files changed, 100 insertions(+) create mode 100644 bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts 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..5a15fbf 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,8 @@ 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); default: return null; } @@ -169,6 +172,43 @@ 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)); + + bridge.runAspectBuild(targets.toArray(new String[0]), null); + + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Pre-debug build complete for " + projectName)); + 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 handleCreateProjectForPackage(List arguments, IProgressMonitor monitor) { try { if (arguments.isEmpty() || !(arguments.get(0) instanceof String)) { 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..b68005e 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml +++ b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml @@ -24,6 +24,7 @@ + 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..8b40fa3 --- /dev/null +++ b/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts @@ -0,0 +1,53 @@ +import * as vscode from 'vscode'; + +export class BazelDebugConfigurationProvider implements vscode.DebugConfigurationProvider { + 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[] = []; From 371f78fbee4e1af32f24224e870fb55c632b5956 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Thu, 21 May 2026 21:51:48 +0800 Subject: [PATCH 3/9] feat: add plain build support for pre-debug target compilation Replace aspect build with a lightweight plain `bazel build` for pre-debug target compilation. This avoids the overhead of aspect evaluation and graph updates when only compilation is needed. Co-Authored-By: Claude Opus 4.6 --- .../crates/bazel-jdt-core/src/jni_exports.rs | 38 +++++++- .../crates/bazel-query/src/command.rs | 29 +++++++ .../main/java/com/bazel/jdt/BazelBridge.java | 18 ++++ .../com/bazel/jdt/BazelCommandHandler.java | 2 +- .../scripts/capture-jdtls-threadump.sh | 87 +++++++++++++++++++ 5 files changed, 172 insertions(+), 2 deletions(-) create mode 100755 bazel-jdt-bridge/scripts/capture-jdtls-threadump.sh 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..4adf9f4 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,42 @@ 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-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..b1f448c 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 @@ -146,6 +146,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 { @@ -337,6 +354,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/BazelCommandHandler.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java index 5a15fbf..b065b9e 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 @@ -197,7 +197,7 @@ private Object handleBuildTarget(List arguments) { LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", "Pre-debug build for " + projectName + ": " + targets)); - bridge.runAspectBuild(targets.toArray(new String[0]), null); + bridge.buildTargets(targets.toArray(new String[0]), null); LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", "Pre-debug build complete for " + projectName)); 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/" From 65bfa98399bde350c119b9a2fd3dd18c0c7793b6 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Thu, 21 May 2026 22:49:52 +0800 Subject: [PATCH 4/9] feat: optimize debug stackTrace with in-memory runtime classpath resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register IRuntimeClasspathEntryResolver for BAZEL_CONTAINER to bypass O(n²) computeDefaultContainerEntries() recursion. Use in-memory JavaCore.getClasspathContainer() instead of file I/O, and scope resolution to the active debug project for O(1) lookup. Co-Authored-By: Claude Opus 4.6 --- .../com/bazel/jdt/BazelClasspathManager.java | 1 + .../com/bazel/jdt/BazelCommandHandler.java | 1 + .../BazelRuntimeClasspathEntryResolver.java | 115 ++++++++++++++++++ .../java-bridge/src/main/resources/plugin.xml | 7 ++ 4 files changed, 124 insertions(+) create mode 100644 bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java 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..ec8e650 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 @@ -124,6 +124,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 b065b9e..4d3f1ee 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 @@ -178,6 +178,7 @@ private Object handleBuildTarget(List arguments) { throw new IllegalArgumentException("Project name required"); } String projectName = (String) arguments.get(0); + BazelRuntimeClasspathEntryResolver.setActiveDebugProject(projectName); IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); if (!project.exists()) { 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..b1c27e5 --- /dev/null +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java @@ -0,0 +1,115 @@ +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 setActiveDebugProject(String projectName) { + activeDebugProject = projectName; + } + + public static void clearActiveDebugProject() { + activeDebugProject = null; + } + + public static void clearCache() { + CACHE.clear(); + } +} 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 b68005e..7dc54d5 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml +++ b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml @@ -28,6 +28,13 @@ + + + + From bec3c332a8172e466128489deff7078eda75c1a8 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Thu, 21 May 2026 23:08:08 +0800 Subject: [PATCH 5/9] style: fix cargo fmt formatting in classpath.rs and jni_exports.rs Co-Authored-By: Claude Opus 4.6 --- bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs | 10 +++------- .../crates/bazel-jdt-core/src/jni_exports.rs | 5 ++++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs index 904aff5..cb5b4d4 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs @@ -1712,13 +1712,9 @@ mod tests { )]; graph.populate_from_aspects(&results, Path::new("/workspace")); - let cp = ComputedClasspath::compute_for( - &graph, - "@maven//:guava", - TargetKind::JavaImport, - None, - ) - .unwrap(); + let cp = + ComputedClasspath::compute_for(&graph, "@maven//:guava", TargetKind::JavaImport, None) + .unwrap(); assert!( !cp.output_jars.is_empty(), 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 4adf9f4..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 @@ -520,7 +520,10 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeBuildTargets( "nativeBuildTargets: plain build for {} targets", target_vec.len() ); - match state.invoker.build_targets_sync(&target_vec, build_flags_ref) { + match state + .invoker + .build_targets_sync(&target_vec, build_flags_ref) + { Ok(()) => { log::info!("Plain build complete"); 1 From 7a5f9ec65fbeaab804d5b382a32590acf3000efd Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Fri, 22 May 2026 11:23:59 +0800 Subject: [PATCH 6/9] feat: collect transitive dep jars for java_binary runtime classpath For java_binary targets, gather output_jars from all transitive dependencies so the debug runtime classpath includes the full set of required jars. Small stub jars (<1KB) are filtered out. After pre-debug build, clear the runtime classpath cache and refresh the classpath container to pick up freshly built artifacts. Co-Authored-By: Claude Opus 4.6 --- .../crates/bazel-graph/src/classpath.rs | 207 +++++++++++++++++- .../com/bazel/jdt/BazelCommandHandler.java | 4 + 2 files changed, 209 insertions(+), 2 deletions(-) diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs index cb5b4d4..1255a86 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs @@ -85,7 +85,7 @@ impl ComputedClasspath { | TargetKind::JavaBinary | TargetKind::JavaTest | TargetKind::Unknown => { - Self::compute_for_library(graph, target_label, is_test, workspace_root) + Self::compute_for_library(graph, target_label, is_test, workspace_root, &target_kind) } } } @@ -176,6 +176,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)?; @@ -244,11 +245,35 @@ 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()) .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.contains(&jar.classpath_path) { + output_jars.push(jar.classpath_path.clone()); + } + } + } + } + + 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, @@ -1765,4 +1790,182 @@ mod tests { "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/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 4d3f1ee..af9e88a 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 @@ -202,6 +202,10 @@ private Object handleBuildTarget(List arguments) { LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", "Pre-debug build complete for " + projectName)); + + BazelRuntimeClasspathEntryResolver.clearCache(); + BazelClasspathManager.setMergedClasspathContainer(project); + return null; } catch (Exception e) { LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", From ff82ac74f4c0ea2447f6f0ebb4d93957e171baa7 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Fri, 22 May 2026 17:59:19 +0800 Subject: [PATCH 7/9] feat: deferred JAR resolution with multi-path fallback for debug classpath Instead of eagerly picking between full/compile JARs at graph population time, store all JAR variants (full, compile, runtime) in ResolvedJar and resolve lazily via effective_path(). This enables accurate classpath resolution for debug sessions where runtime JARs may only appear after a build. Also centralizes BazelProjectView in BazelBridge and adds build failure detection for pre-debug builds. Co-Authored-By: Claude Opus 4.6 --- .../crates/bazel-aspect/src/ide_info.rs | 41 ++ .../crates/bazel-aspect/src/text_proto.rs | 51 ++ .../crates/bazel-graph/src/classpath.rs | 72 +-- .../crates/bazel-graph/src/graph.rs | 501 ++++++++++++++++-- .../bazel-jdt-core/src/aspects/artifacts.bzl | 4 +- .../bazel-jdt-core/tests/integration.rs | 20 +- .../main/java/com/bazel/jdt/BazelBridge.java | 18 + .../com/bazel/jdt/BazelClasspathManager.java | 16 +- .../com/bazel/jdt/BazelCommandHandler.java | 24 +- .../com/bazel/jdt/BazelProjectImporter.java | 29 +- 10 files changed, 653 insertions(+), 123 deletions(-) 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 1255a86..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, &target_kind) - } + | TargetKind::Unknown => Self::compute_for_library( + graph, + target_label, + is_test, + workspace_root, + &target_kind, + ), } } @@ -221,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() { @@ -230,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, @@ -247,7 +251,11 @@ impl ComputedClasspath { 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 { @@ -257,19 +265,17 @@ impl ComputedClasspath { } if let Some(dep_jars) = graph.get_target_jars(dep_label) { for jar in dep_jars { - if !output_jars.contains(&jar.classpath_path) { - output_jars.push(jar.classpath_path.clone()); + 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, - } + output_jars.retain(|jar_path| match std::fs::metadata(jar_path) { + Ok(meta) => meta.len() >= 1024, + Err(_) => true, }); } } @@ -302,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, @@ -314,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 { @@ -965,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, }], ); @@ -976,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] @@ -991,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, }], ); @@ -1844,9 +1858,8 @@ mod tests { ]; graph.populate_from_aspects(&results, Path::new("/workspace")); - let cp = - ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None) - .unwrap(); + let cp = ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None) + .unwrap(); assert!( cp.output_jars @@ -1877,9 +1890,8 @@ mod tests { ]; graph.populate_from_aspects(&results, Path::new("/workspace")); - let cp = - ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None) - .unwrap(); + let cp = ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None) + .unwrap(); assert!( cp.output_jars @@ -1916,9 +1928,8 @@ mod tests { ]; graph.populate_from_aspects(&results, Path::new("/workspace")); - let cp = - ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None) - .unwrap(); + let cp = ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None) + .unwrap(); let lines = cp.to_pipe_delimited_entries(); let lib_count = lines @@ -1953,9 +1964,8 @@ mod tests { ]; graph.populate_from_aspects(&results, Path::new("/workspace")); - let cp = - ComputedClasspath::compute_for(&graph, "//lib:lib", TargetKind::JavaLibrary, None) - .unwrap(); + let cp = ComputedClasspath::compute_for(&graph, "//lib:lib", TargetKind::JavaLibrary, None) + .unwrap(); assert_eq!( cp.output_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..3ff1b5b 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,201 @@ 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 +1803,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 +1812,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 +1863,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 +1882,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 +2033,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 +2082,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 +2389,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 +2428,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 +2469,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 +2517,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 +2583,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 +2710,8 @@ 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 +2751,8 @@ 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 +2790,8 @@ 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 +2867,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 +2915,8 @@ 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 +2954,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 +3094,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/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/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 b1f448c..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(); } @@ -251,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 { 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 ec8e650..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( 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 af9e88a..4d642ba 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 @@ -62,13 +62,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) { @@ -92,7 +88,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) { @@ -198,13 +194,21 @@ private Object handleBuildTarget(List arguments) { LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", "Pre-debug build for " + projectName + ": " + targets)); - bridge.buildTargets(targets.toArray(new String[0]), null); + 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.clearCache(); - BazelClasspathManager.setMergedClasspathContainer(project); + BazelClasspathContainer.resetWarnings(); + BazelClasspathManager.setMergedClasspathContainer(project, true); return null; } catch (Exception e) { 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; From c7f531e5ce9dc440880e611498f88640b66082ae Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Fri, 22 May 2026 21:48:39 +0800 Subject: [PATCH 8/9] refactor: separate debug project activation from build and use targeted cache invalidation Extract setActiveDebugProject into a standalone JDT command invoked early in resolveDebugConfiguration, so the runtime classpath filter is active before the build step. Replace full cache clear with per-project clearCacheForProject for more precise invalidation. Co-Authored-By: Claude Opus 4.6 --- .../com/bazel/jdt/BazelCommandHandler.java | 14 ++++++++++--- .../BazelRuntimeClasspathEntryResolver.java | 13 +++++++----- .../java-bridge/src/main/resources/plugin.xml | 1 + .../vscode-extension/src/debugAdapter.ts | 20 +++++++++++++++++++ 4 files changed, 40 insertions(+), 8 deletions(-) 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 4d642ba..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 @@ -37,6 +37,8 @@ public Object executeCommand(String commandId, List arguments, IProgress return handleWaitForIndexesReady(); case "bazel-jdt.buildTarget": return handleBuildTarget(arguments); + case "bazel-jdt.setActiveDebugProject": + return handleSetActiveDebugProject(arguments); default: return null; } @@ -174,7 +176,6 @@ private Object handleBuildTarget(List arguments) { throw new IllegalArgumentException("Project name required"); } String projectName = (String) arguments.get(0); - BazelRuntimeClasspathEntryResolver.setActiveDebugProject(projectName); IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); if (!project.exists()) { @@ -206,9 +207,9 @@ private Object handleBuildTarget(List arguments) { LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", "Pre-debug build complete for " + projectName)); - BazelRuntimeClasspathEntryResolver.clearCache(); + BazelRuntimeClasspathEntryResolver.clearCacheForProject(projectName); BazelClasspathContainer.resetWarnings(); - BazelClasspathManager.setMergedClasspathContainer(project, true); + BazelClasspathManager.setMergedClasspathContainer(project, false); return null; } catch (Exception e) { @@ -218,6 +219,13 @@ private Object handleBuildTarget(List arguments) { } } + 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/BazelRuntimeClasspathEntryResolver.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java index b1c27e5..24bc16f 100644 --- 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 @@ -23,7 +23,6 @@ public class BazelRuntimeClasspathEntryResolver implements IRuntimeClasspathEntr 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 @@ -101,6 +100,14 @@ private IRuntimeClasspathEntry[] buildEntries(IJavaProject project) { } } + public static void clearCache() { + CACHE.clear(); + } + + public static void clearCacheForProject(String projectName) { + CACHE.remove(projectName); + } + public static void setActiveDebugProject(String projectName) { activeDebugProject = projectName; } @@ -108,8 +115,4 @@ public static void setActiveDebugProject(String projectName) { public static void clearActiveDebugProject() { activeDebugProject = null; } - - public static void clearCache() { - CACHE.clear(); - } } 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 7dc54d5..59de6c0 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml +++ b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml @@ -25,6 +25,7 @@ + diff --git a/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts b/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts index 8b40fa3..c1c64a9 100644 --- a/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts +++ b/bazel-jdt-bridge/vscode-extension/src/debugAdapter.ts @@ -1,6 +1,26 @@ 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, From 8b8d1c7cdc6bca10f732a083c8aa2b6e2120f0ae Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Fri, 22 May 2026 21:51:33 +0800 Subject: [PATCH 9/9] style: fix cargo fmt formatting in graph.rs Co-Authored-By: Claude Opus 4.6 --- .../crates/bazel-graph/src/graph.rs | 65 +++++++++++++++---- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs b/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs index 3ff1b5b..c3e663d 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs @@ -1595,7 +1595,10 @@ mod tests { source_path: None, }; - assert_eq!(jar.effective_path(), runtime_file.to_string_lossy().as_ref()); + assert_eq!( + jar.effective_path(), + runtime_file.to_string_lossy().as_ref() + ); let _ = std::fs::remove_file(&runtime_file); } @@ -1694,8 +1697,14 @@ mod tests { 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"); + let resolved = build_resolved_jars( + &java_info, + &full_jars, + &[], + &runtime_jars, + &workspace, + "//foo:lib", + ); assert_eq!(resolved.len(), 1); assert_eq!( @@ -1714,8 +1723,14 @@ mod tests { "/execroot/external/bar/classes.jar".to_string(), ]; - let resolved = - build_resolved_jars(&java_info, &full_jars, &[], &runtime_jars, &workspace, "//foo:lib"); + let resolved = build_resolved_jars( + &java_info, + &full_jars, + &[], + &runtime_jars, + &workspace, + "//foo:lib", + ); assert_eq!(resolved.len(), 1); assert!( @@ -2710,8 +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!( @@ -2751,8 +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!( @@ -2790,8 +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!( @@ -2915,8 +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!(