diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java index d37697e7..593a8bdb 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java @@ -1262,7 +1262,28 @@ private IPath findCommonParentPackagePrefix(Collection detectedJavaPackag } } - return null; + // none of the detected packages is a prefix of all others (sibling packages case) + // compute the actual longest common prefix path + // e.g. for [com/foo/bar/dto, com/foo/bar/merge] this yields com/foo/bar + IPath commonPrefix = null; + for (IPath path : detectedJavaPackagesForSourceDirectory) { + if (commonPrefix == null) { + commonPrefix = path; + } else { + var minLen = Math.min(commonPrefix.segmentCount(), path.segmentCount()); + var commonLen = 0; + for (var i = 0; i < minLen; i++) { + if (commonPrefix.segment(i).equals(path.segment(i))) { + commonLen = i + 1; + } else { + break; + } + } + commonPrefix = commonPrefix.uptoSegment(commonLen); + } + } + + return (commonPrefix == null) || commonPrefix.isEmpty() ? null : commonPrefix; } private IProject findProjectForLocation(IPath location) { diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java index 6c166b96..6d390eaa 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java @@ -358,6 +358,8 @@ public CompileAndRuntimeClasspath compute() throws CoreException { var libraryArtifact = new LibraryArtifact(artifact, classJar, srcJars); var targetKey = targetLabel != null ? TargetKey.forPlainTarget(targetLabel) : null; library = new BlazeJarLibrary(libraryArtifact, targetKey); + // Register back to avoid repeated fallback for the same jar across multiple targets + aspectsInfo.registerFallbackLibrary(artifact.getRelativePath(), library); } var entry = resolveLibrary(library); if (entry != null) { @@ -375,9 +377,14 @@ public CompileAndRuntimeClasspath compute() throws CoreException { classpathBuilder.addCompileEntry(entry); } else { entry.getAccessRules().add(new AccessRule(PATTERN_EVERYTHING, IAccessRule.K_ACCESSIBLE)); + // Export EXPLICIT jdeps entries so downstream projects can resolve types + // referenced in this project's public API. This addresses ECJ vs javac differences: + // ECJ aggressively resolves all types in the API chain, while javac may not + // record them in jdeps of downstream targets. + entry.setExported(true); classpathBuilder.addCompileEntry(entry); } - } else if (LOG.isDebugEnabled()) { + } else { LOG.warn("Unable to resolve compile jar: {}", jdepsDependency); } } diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsInfo.java index ea32e190..6be5dbc9 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsInfo.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsInfo.java @@ -261,6 +261,18 @@ private void addLibrary(BlazeJarLibrary library) { var classJar = libraryArtifact.getClassJar(); if (classJar != null) { libraryByJdepsRootRelativePath.put(classJar.getRelativePath(), library); + + // When there is no interfaceJar (e.g., libraries discovered from runtime classpath), + // also index under potential ijar/hjar paths so that jdeps lookups can find them. + // jdeps files typically reference ijar/hjar paths, not class jar paths. + if (interfaceJar == null) { + var classPath = classJar.getRelativePath(); + if (classPath.endsWith(".jar")) { + var base = classPath.substring(0, classPath.length() - ".jar".length()); + libraryByJdepsRootRelativePath.putIfAbsent(base + "-ijar.jar", library); + libraryByJdepsRootRelativePath.putIfAbsent(base + "-hjar.jar", library); + } + } } } @@ -276,6 +288,14 @@ public BlazeJarLibrary getLibraryByJdepsRootRelativePath(String relativePath) { return libraryByJdepsRootRelativePath.get(relativePath); } + /** + * Registers a fallback library discovered during jdeps resolution so that subsequent lookups for the same path + * don't need to repeat the fallback logic. + */ + public void registerFallbackLibrary(String relativePath, BlazeJarLibrary library) { + libraryByJdepsRootRelativePath.putIfAbsent(relativePath, library); + } + /** * Checks if a JAR file comes from an external Bazel repository. *

diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java index bbac2b75..4f17288b 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java @@ -1,6 +1,7 @@ package com.salesforce.bazel.eclipse.core.model.discovery; import static java.lang.String.format; +import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; import static org.eclipse.core.runtime.SubMonitor.SUPPRESS_ALL_LABELS; @@ -8,13 +9,15 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.SubMonitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,10 +32,12 @@ import com.salesforce.bazel.eclipse.core.model.BazelTarget; import com.salesforce.bazel.eclipse.core.model.BazelWorkspace; import com.salesforce.bazel.eclipse.core.model.BazelWorkspaceBlazeInfo; +import com.salesforce.bazel.eclipse.core.model.discovery.projects.JavaProjectInfo; import com.salesforce.bazel.eclipse.core.util.trace.TracingSubMonitor; import com.salesforce.bazel.sdk.aspects.intellij.IntellijAspects; import com.salesforce.bazel.sdk.aspects.intellij.IntellijAspects.OutputGroup; import com.salesforce.bazel.sdk.command.BazelBuildWithIntelliJAspectsCommand; +import com.salesforce.bazel.sdk.command.querylight.BazelRuleAttribute; import com.salesforce.bazel.sdk.model.BazelLabel; /** @@ -62,21 +67,17 @@ public class ProjectPerTargetProvisioningStrategy extends BaseProvisioningStrate * packages that may not exist in the workspace. */ private void addPackageForLabel(Label label, BazelWorkspace workspace, Set packages) { - // Skip external packages as they may not have corresponding workspace packages if (label.isExternal()) { - LOG.trace("Skipping external label: {}", label); return; } - - var bazelPackage = workspace.getBazelPackage(new BazelLabel(label)); - - // Only add if the package exists and is accessible - if (!bazelPackage.exists()) { - LOG.trace("Skipping non-existing package label: {}", label); - return; + try { + var bazelPackage = workspace.getBazelPackage(new BazelLabel(label)); + if (bazelPackage.exists()) { + packages.add(bazelPackage); + } + } catch (IllegalArgumentException e) { + LOG.trace("Skipping label not rooted at workspace: {}", label); } - - packages.add(bazelPackage); } /** @@ -93,36 +94,42 @@ private void addPackageForLabel(Label label, BazelWorkspace workspace, Set packages) { + var targetKey = TargetKey.forPlainTarget(target.getLabel().toPrimitive()); + var targetInfo = aspectsInfo.get(targetKey); + + if (targetInfo != null) { + for (var dep : targetInfo.getDependencies()) { + addPackageForLabel(dep.getTargetKey().getLabel(), workspace, packages); + } + + var runtimeClasspath = aspectsInfo.getRuntimeClasspath(targetKey); + if (runtimeClasspath != null) { + for (var jar : runtimeClasspath) { + if (jar.targetKey != null) { + addPackageForLabel(jar.targetKey.getLabel(), workspace, packages); + } + } + } + } + } + private Set collectDependencyPackages(Collection bazelProjects, JavaAspectsInfo aspectsInfo, BazelWorkspace workspace) throws CoreException { Set packages = new HashSet<>(); for (BazelProject bazelProject : bazelProjects) { - // Skip projects that don't have a proper target yet if (!bazelProject.isTargetProject()) { - LOG.trace("Skipping project {} - not a target project or target is null", bazelProject.getName()); continue; } - // Collect all dependency labels from the aspects info - var targetKey = TargetKey.forPlainTarget(bazelProject.getBazelTarget().getLabel().toPrimitive()); - var targetInfo = aspectsInfo.get(targetKey); - - if (targetInfo != null) { - // Collect packages from direct dependencies - for (var dep : targetInfo.getDependencies()) { - addPackageForLabel(dep.getTargetKey().getLabel(), workspace, packages); - } + // Collect deps from the primary target + collectDependencyPackagesForTarget(bazelProject.getBazelTarget(), aspectsInfo, workspace, packages); - // Collect packages from runtime dependencies - var runtimeClasspath = aspectsInfo.getRuntimeClasspath(targetKey); - if (runtimeClasspath != null) { - for (var jar : runtimeClasspath) { - if (jar.targetKey != null) { - addPackageForLabel(jar.targetKey.getLabel(), workspace, packages); - } - } - } + // Also collect deps from unprovisioned sibling targets in the same package + for (BazelTarget sibling : getUnprovisionedSiblingTargets(bazelProject)) { + collectDependencyPackagesForTarget(sibling, aspectsInfo, workspace, packages); } } @@ -136,19 +143,37 @@ public Map computeClasspaths(Collectio try { var monitor = SubMonitor.convert(progress, "Computing Bazel project classpaths", 1 + bazelProjects.size()); + // Build targets list including unprovisioned sibling targets List targetsToBuild = new ArrayList<>(bazelProjects.size()); + Set addedLabels = new HashSet<>(); + Map> siblingTargetsMap = new HashMap<>(); + for (BazelProject bazelProject : bazelProjects) { monitor.checkCanceled(); if (!bazelProject.isTargetProject()) { - throw new CoreException( - Status.error( - format( - "Unable to compute classpath for project '%s'. Please check the setup. This is not a Bazel target project created by the project per target strategy.", - bazelProjects))); + LOG.warn("Skipping non-target project '{}' in classpath computation", bazelProject.getName()); + continue; + } + + // Add the primary target + var primaryLabel = bazelProject.getBazelTarget().getLabel(); + if (addedLabels.add(primaryLabel)) { + targetsToBuild.add(primaryLabel); } - targetsToBuild.add(bazelProject.getBazelTarget().getLabel()); + // Discover and add unprovisioned sibling targets from the same package + var siblings = getUnprovisionedSiblingTargets(bazelProject); + siblingTargetsMap.put(bazelProject, siblings); + for (BazelTarget sibling : siblings) { + if (addedLabels.add(sibling.getLabel())) { + targetsToBuild.add(sibling.getLabel()); + } + } + } + + if (targetsToBuild.isEmpty()) { + return Map.of(); } var workspaceRoot = workspace.getLocation().toPath(); @@ -188,19 +213,27 @@ public Map computeClasspaths(Collectio var aspectsInfo = new JavaAspectsInfo(result, workspace, aspects); // Performance optimization: Pre-load all dependency packages to avoid repeated Bazel queries - // This utilizes the existing batch optimization in BazelWorkspace.open() - var packagesToPreload = collectDependencyPackages(bazelProjects, aspectsInfo, workspace); - if (!packagesToPreload.isEmpty()) { - LOG.debug( - "Pre-loading {} dependency packages to optimize classpath computation", - packagesToPreload.size()); - workspace.open(packagesToPreload); + try { + var packagesToPreload = collectDependencyPackages(bazelProjects, aspectsInfo, workspace); + if (!packagesToPreload.isEmpty()) { + LOG.debug( + "Pre-loading {} dependency packages to optimize classpath computation", + packagesToPreload.size()); + workspace.open(packagesToPreload); + } + } catch (Exception e) { + LOG.warn("Failed to pre-load dependency packages, continuing without optimization", e); } for (BazelProject bazelProject : bazelProjects) { monitor.subTask(bazelProject.getName()); monitor.checkCanceled(); + if (!bazelProject.isTargetProject()) { + monitor.worked(1); + continue; + } + // build index of classpath info var classpathInfo = new JavaAspectsClasspathInfo(aspectsInfo, workspace, availableDependencies, bazelProject); @@ -208,12 +241,21 @@ public Map computeClasspaths(Collectio // remove old marker deleteClasspathContainerProblems(bazelProject); - // add the target + // add the primary target var problem = classpathInfo.addTarget(bazelProject.getBazelTarget()); if (!problem.isOK()) { createClasspathContainerProblem(bazelProject, problem); } + // add unprovisioned sibling targets so their deps are included in the classpath + var siblings = siblingTargetsMap.getOrDefault(bazelProject, List.of()); + for (BazelTarget sibling : siblings) { + var siblingProblem = classpathInfo.addTarget(sibling); + if (!siblingProblem.isOK()) { + createClasspathContainerProblem(bazelProject, siblingProblem); + } + } + // compute the classpath var classpath = classpathInfo.compute(); @@ -229,23 +271,139 @@ public Map computeClasspaths(Collectio } } + private JavaProjectInfo collectJavaInfoForAnalysis(BazelPackage bazelPackage, BazelTarget sampleTarget) + throws CoreException { + var javaInfo = new JavaProjectInfo(bazelPackage); + var attributes = sampleTarget.getRuleAttributes(); + var srcs = attributes.getStringList(BazelRuleAttribute.SRCS); + if (srcs != null) { + for (String src : srcs) { + javaInfo.addSrc(src, null); + } + } + javaInfo.analyzeProjectRecommendations(false, new NullProgressMonitor()); + return javaInfo; + } + @Override protected List doProvisionProjects(Collection targets, TracingSubMonitor monitor) throws CoreException { monitor.setWorkRemaining(targets.size()); List result = new ArrayList<>(); - for (BazelTarget target : targets) { - monitor.subTask(target.getLabel().toString()); - // provision project - var project = provisionProjectForTarget(target, monitor); - if (project != null) { - result.add(project); + var targetsByPackage = targets.stream() + .collect(groupingBy(BazelTarget::getBazelPackage, LinkedHashMap::new, toList())); + + for (var entry : targetsByPackage.entrySet()) { + var bazelPackage = entry.getKey(); + var packageTargets = entry.getValue(); + + if (packageTargets.size() > 1 && hasEmptySourceRoot(bazelPackage, packageTargets)) { + LOG.debug("Detected empty source root for package '{}', merging {} targets into one project", + bazelPackage, packageTargets.size()); + var project = provisionMergedTargetProject(bazelPackage, packageTargets, monitor); + if (project != null) { + result.add(project); + } + } else { + for (BazelTarget target : packageTargets) { + monitor.subTask(target.getLabel().toString()); + var project = provisionProjectForTarget(target, monitor); + if (project != null) { + result.add(project); + } + } } } return result; } + /** + * Returns unprovisioned sibling java_* targets in the same package. For merged projects, these are the targets that + * were skipped during provisioning but whose dependencies should be included in classpath computation. + */ + private List getUnprovisionedSiblingTargets(BazelProject bazelProject) throws CoreException { + var primaryTarget = bazelProject.getBazelTarget(); + var bazelPackage = primaryTarget.getBazelPackage(); + List siblings = new ArrayList<>(); + for (BazelTarget t : bazelPackage.getBazelTargets()) { + if (t.getTargetName().equals(primaryTarget.getTargetName())) { + continue; + } + if (t.hasBazelProject()) { + continue; + } + if (isJavaRule(t.getRuleClass())) { + siblings.add(t); + } + } + return siblings; + } + + /** + * Merges multiple targets sharing an empty source root into a single target project. The primary target (the + * java_library with the most sources) is used for the project identity, but all targets' source files are included. + */ + private BazelProject provisionMergedTargetProject(BazelPackage bazelPackage, List allTargets, + TracingSubMonitor monitor) throws CoreException { + var primaryTarget = selectPrimaryTarget(allTargets); + monitor.subTask(primaryTarget.getLabel().toString()); + monitor = monitor.split(1, "Provisioning merged project for " + primaryTarget.getLabel()); + + var project = provisionTargetProject(primaryTarget, monitor.slice(1)); + + // Build Java info from ALL targets in the package + var javaInfo = collectJavaInfo(project, allTargets, monitor.slice(1)); + + // Configure links and classpath using combined info + linkSourcesIntoProject(project, javaInfo, monitor.slice(1)); + linkGeneratedSourcesIntoProject(project, javaInfo, monitor.slice(1)); + linkJarsIntoProject(project, javaInfo, monitor.slice(1)); + configureRawClasspath(project, javaInfo, monitor.slice(1)); + + return project; + } + + /** + * Selects the primary target from a list of targets. Prefers the java_library with the most source files. + */ + private BazelTarget selectPrimaryTarget(List targets) throws CoreException { + BazelTarget primary = null; + int maxSrcs = -1; + for (BazelTarget target : targets) { + if (!"java_library".equals(target.getRuleClass())) { + continue; + } + var srcs = target.getRuleAttributes().getStringList(BazelRuleAttribute.SRCS); + int count = (srcs != null) ? srcs.size() : 0; + if (count > maxSrcs) { + maxSrcs = count; + primary = target; + } + } + return primary != null ? primary : targets.get(0); + } + + private boolean hasEmptySourceRoot(BazelPackage bazelPackage, List targets) { + for (BazelTarget target : targets) { + try { + var javaInfo = collectJavaInfoForAnalysis(bazelPackage, target); + if (javaInfo.getSourceInfo().hasSourceDirectories() + && javaInfo.getSourceInfo().getSourceDirectories().stream().anyMatch(IPath::isEmpty)) { + return true; + } + } catch (CoreException e) { + LOG.debug("Failed to analyze target '{}': {}", target, e.getMessage()); + } + } + return false; + } + + private boolean isJavaRule(String ruleClass) { + return "java_library".equals(ruleClass) || "java_import".equals(ruleClass) + || "java_binary".equals(ruleClass) || "java_test".equals(ruleClass); + } + protected BazelProject provisionJavaBinaryProject(BazelTarget target, TracingSubMonitor monitor) throws CoreException { // TODO: create a shared launch configuration