diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelProjectFileSystemMapper.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelProjectFileSystemMapper.java index bac82ecb..a5732038 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelProjectFileSystemMapper.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelProjectFileSystemMapper.java @@ -60,6 +60,28 @@ public BazelWorkspace getBazelWorkspace() { return bazelWorkspace; } + /** + * Returns the folder where source files links to generated sources will be created. + *

+ * This can be used whenever there is a need to bring generated sources into a project (eg., .srcjar ). + * It is expected that this folder is used as a parent. It should only contain symlinks to folders with the + * generated sources. Do not copy generated sources in here. + *

+ * + * @param project + * @return the folder to use for generated sources + */ + public IFolder getGeneratedSourcesFolder(BazelProject project) { + return project.getProject().getFolder("generated-srcs"); + } + + /** + * @see #getGeneratedSourcesFolder(BazelProject) + */ + public IFolder getGeneratedSourcesFolderForTests(BazelProject project) { + return project.getProject().getFolder("generated-test-srcs"); + } + /** * @see #getVirtualSourceFolder(BazelProject) */ diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java index 202196fe..67c09126 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/BazelWorkspace.java @@ -199,6 +199,16 @@ BazelBinary getBazelBinary() throws CoreException { return getInfo().getBazelBinary(); } + /** + * {@return absolute file system location to the bazel-bin symlink target} + * + * @throws CoreException + * if the workspace does not exist + */ + public IPath getBazelBinLocation() throws CoreException { + return getInfo().getBazelBin(); + } + /** * Returns a Bazel package for the given label. *

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 d02e2365..8698302f 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 @@ -89,6 +89,7 @@ import com.salesforce.bazel.eclipse.core.model.discovery.projects.JavaResourceInfo; import com.salesforce.bazel.eclipse.core.model.discovery.projects.JavaSourceEntry; import com.salesforce.bazel.eclipse.core.model.discovery.projects.JavaSourceInfo; +import com.salesforce.bazel.eclipse.core.model.discovery.projects.JavaSrcJarEntry; import com.salesforce.bazel.sdk.command.BazelCQueryWithStarlarkExpressionCommand; /** @@ -239,6 +240,36 @@ private void addSourceFolders(BazelProject project, List rawCla } if (javaSourceInfo.hasSourceDirectories()) { for (IPath dir : javaSourceInfo.getSourceDirectories()) { + // check for srcjar + if (dir.isAbsolute()) { + // double check + if (!javaSourceInfo.matchAllSourceDirectoryEntries(dir, JavaSrcJarEntry.class::isInstance)) { + throw new IllegalStateException( + format("programming error: found unsupported content for source directory '%s'", dir)); + } + + var srcjarFolder = + getFileSystemMapper().getGeneratedSourcesFolder(project).getFolder(dir.lastSegment()); + if (!srcjarFolder.exists()) { + createBuildPathProblem( + project, + Status.error( + format( + "Folder '%s' does not exist. However, it's required for the project classpath to resolve properly. Please investigat!", + srcjarFolder))); + } else { + rawClasspath.add( + JavaCore.newSourceEntry( + srcjarFolder.getFullPath(), + null, + null, + outputLocation, + classpathAttributes)); + } + + continue; + } + // when the directory is empty, the virtual "srcs" container must be used // this logic here requires proper linking support in linkSourcesIntoProject method var sourceFolder = dir.isEmpty() ? virtualSourceFolder : project.getProject().getFolder(dir); @@ -874,6 +905,14 @@ protected void doInitializeClasspaths(List projects, BazelWorkspac protected abstract List doProvisionProjects(Collection targets, SubMonitor monitor) throws CoreException; + private void ensureFolderLinksToTarget(IFolder folderWhichShouldBeALink, IPath linkTarget, SubMonitor monitor) + throws CoreException { + if (folderWhichShouldBeALink.exists() && !folderWhichShouldBeALink.isLinked()) { + folderWhichShouldBeALink.delete(true, monitor.split(1)); + } + folderWhichShouldBeALink.createLink(linkTarget, IResource.REPLACE, monitor.split(1)); + } + protected BazelPackage expectCommonBazelPackage(Collection targets) throws CoreException { List allPackages = targets.stream().map(BazelTarget::getBazelPackage).distinct().collect(toList()); @@ -920,6 +959,23 @@ private IProject findProjectForLocation(IPath location) { return null; } + private void garbageCollectFolderContent(IFolder folder, Set membersToRetain, SubMonitor monitor) + throws CoreException { + if (!folder.exists()) { + return; + } + + for (IResource member : folder.members()) { + if (!membersToRetain.contains(member)) { + member.delete(IResource.NONE, monitor.split(1)); + } + } + + if (folder.members().length == 0) { + folder.delete(IResource.NONE, monitor.split(1)); + } + } + protected IWorkspace getEclipseWorkspace() { return ResourcesPlugin.getWorkspace(); } @@ -993,150 +1049,242 @@ private boolean isTestTarget(BazelTarget bazelTarget) throws CoreException { return bazelTarget.getRuleClass().contains("test"); } + private void linkGeneratedSourceDirectories(JavaSourceInfo sourceInfo, IFolder generatedSourcesFolder, + SubMonitor monitor) throws CoreException { + if (!sourceInfo.hasSourceDirectories()) { + return; + } + + var directories = sourceInfo.getSourceDirectories(); + + // capture created srcjar folders for GC later + var generatedSourcesMembers = new HashSet(); + + // check each directory + for (IPath dir : directories) { + // ignore non-srcjars + if (!dir.isAbsolute()) { + continue; + } + + // double check + if (!sourceInfo.matchAllSourceDirectoryEntries(dir, JavaSrcJarEntry.class::isInstance)) { + throw new IllegalStateException( + format("programming error: found unsupported content for source directory '%s'", dir)); + } + + if (!generatedSourcesFolder.exists()) { + createFolderAndParents(generatedSourcesFolder, monitor.split(1)); + } + + var srcjarFolder = generatedSourcesFolder.getFolder(dir.lastSegment()); + ensureFolderLinksToTarget(srcjarFolder, dir, monitor); + generatedSourcesMembers.add(srcjarFolder); + } + + // cleanup any old generated srcjar folders + garbageCollectFolderContent(generatedSourcesFolder, generatedSourcesMembers, monitor); + } + /** - * Creates Eclipse virtual files/folders for sources collected in the {@link JavaProjectInfo}. + * Creates Eclipse virtual folders for generated sources collected in the {@link JavaSourceInfo}. * * @param project - * @param javaInfo + * @param sourceInfo * @param progress * @throws CoreException */ - protected void linkSourcesIntoProject(BazelProject project, JavaProjectInfo javaInfo, IProgressMonitor progress) - throws CoreException { + protected void linkGeneratedSourcesIntoProject(BazelProject project, JavaProjectInfo javaInfo, + IProgressMonitor progress) throws CoreException { var monitor = SubMonitor.convert(progress, 100); try { - var sourceInfo = javaInfo.getSourceInfo(); + linkGeneratedSourceDirectories( + javaInfo.getSourceInfo(), + getFileSystemMapper().getGeneratedSourcesFolder(project), + monitor); + linkGeneratedSourceDirectories( + javaInfo.getTestSourceInfo(), + getFileSystemMapper().getGeneratedSourcesFolderForTests(project), + monitor); + } finally { + progress.done(); + } + } + + private void linkSourceDirectories(BazelProject project, JavaProjectInfo javaInfo, JavaSourceInfo sourceInfo, + IFolder virtualSourceFolder, IFolder generatedSourcesFolder, SubMonitor monitor) throws CoreException { + if (!sourceInfo.hasSourceDirectories()) { + return; + } + + var directories = sourceInfo.getSourceDirectories(); + + // check each directory + NEXT_FOLDER: for (IPath dir : directories) { + // ignore srcjars + if (dir.isAbsolute()) { + continue; + } + + IFolder sourceFolder; + if (dir.isEmpty()) { + // special case ... source is directly within the project + // this is usually the case when the Bazel package is a Java package + // in this case we need to link (emulate) its package structure + // however, we can only support this properly if this is the only folder + if (directories.size() > 1) { + createBuildPathProblem( + project, + Status.error( + "Impossible to support project: found multiple source directories which seems to be nested! Please consider restructuring the targets.")); + continue NEXT_FOLDER; + } + // and there aren't any other source files to be linked + if (sourceInfo.hasSourceFilesWithoutCommonRoot()) { + createBuildPathProblem( + project, + Status.error( + "Impossible to support project: found mix of source files without common root and empty package fragment root! Please consider restructuring the targets.")); + continue NEXT_FOLDER; + } + // check this maps to a single Java package + var detectedJavaPackagesForSourceDirectory = sourceInfo.getDetectedJavaPackagesForSourceDirectory(dir); + var packagePath = findCommonParentPackagePrefix(detectedJavaPackagesForSourceDirectory); + if (packagePath == null) { + createBuildPathProblem( + project, + Status.error( + format( + "Impossible to support project: an empty package fragment root must map to one Java package (got '%s')! Please consider restructuring the targets.", + detectedJavaPackagesForSourceDirectory.isEmpty() ? "none" + : detectedJavaPackagesForSourceDirectory.stream() + .map(IPath::toString) + .collect(joining(", "))))); + continue NEXT_FOLDER; + } - if (sourceInfo.hasSourceFilesWithoutCommonRoot()) { // create the "srcs" folder - var virtualSourceFolder = getFileSystemMapper().getVirtualSourceFolder(project); + if (virtualSourceFolder.exists() && virtualSourceFolder.isLinked()) { + // delete it to ensure we start fresh + virtualSourceFolder.delete(true, monitor.split(1)); + } createFolderAndParents(virtualSourceFolder, monitor.split(1)); - // build emulated Java package structure and link files - var files = sourceInfo.getSourceFilesWithoutCommonRoot(); - Set linkedFiles = new HashSet<>(); - for (JavaSourceEntry fileEntry : files) { - // peek at Java package to find proper "root" - var packagePath = fileEntry.getDetectedPackagePath(); - var packageFolder = virtualSourceFolder.getFolder(packagePath); - if (!packageFolder.exists()) { - createFolderAndParents(packageFolder, monitor.split(1)); - } - - // create link to file - var file = packageFolder.getFile(fileEntry.getPath().lastSegment()); - file.createLink(fileEntry.getLocation(), IResource.REPLACE, monitor.split(1)); - // remember for cleanup - linkedFiles.add(file); + // build emulated Java package structure and link the directory + var packageFolder = virtualSourceFolder.getFolder(packagePath); + if (!packageFolder.getParent().exists()) { + createFolderAndParents(packageFolder.getParent(), monitor.split(1)); } + ensureFolderLinksToTarget(packageFolder, javaInfo.getBazelPackage().getLocation(), monitor); // remove all files not created as part of this loop - deleteAllFilesNotInAllowList(virtualSourceFolder, linkedFiles, monitor.split(1)); + deleteAllFilesNotInFolderList(virtualSourceFolder, packageFolder, monitor.split(1)); + + // done + break; } - if (sourceInfo.hasSourceDirectories()) { - var directories = sourceInfo.getSourceDirectories(); - NEXT_FOLDER: for (IPath dir : directories) { - IFolder sourceFolder; - if (dir.isEmpty()) { - // special case ... source is directly within the project - // this is usually the case when the Bazel package is a Java package - // in this case we need to link (emulate) its package structure - // however, we can only support this properly if this is the only folder - if (directories.size() > 1) { - createBuildPathProblem( - project, - Status.error( - "Impossible to support project: found multiple source directories which seems to be nested! Please consider restructuring the targets.")); - continue NEXT_FOLDER; - } - // and there aren't any other source files to be linked - if (sourceInfo.hasSourceFilesWithoutCommonRoot()) { - createBuildPathProblem( - project, - Status.error( - "Impossible to support project: found mix of source files without common root and empty package fragment root! Please consider restructuring the targets.")); - continue NEXT_FOLDER; - } - // check this maps to a single Java package - var detectedJavaPackagesForSourceDirectory = - sourceInfo.getDetectedJavaPackagesForSourceDirectory(dir); - var packagePath = findCommonParentPackagePrefix(detectedJavaPackagesForSourceDirectory); - if (packagePath == null) { - createBuildPathProblem( - project, - Status.error( - format( - "Impossible to support project: an empty package fragment root must map to one Java package (got '%s')! Please consider restructuring the targets.", - detectedJavaPackagesForSourceDirectory.isEmpty() ? "none" - : detectedJavaPackagesForSourceDirectory.stream() - .map(IPath::toString) - .collect(joining(", "))))); - continue NEXT_FOLDER; - } - - // create the "srcs" folder - var virtualSourceFolder = getFileSystemMapper().getVirtualSourceFolder(project); - if (virtualSourceFolder.exists() && virtualSourceFolder.isLinked()) { - // delete it to ensure we start fresh - virtualSourceFolder.delete(true, monitor.split(1)); - } - createFolderAndParents(virtualSourceFolder, monitor.split(1)); - - // build emulated Java package structure and link the directory - var packageFolder = virtualSourceFolder.getFolder(packagePath); - if (!packageFolder.getParent().exists()) { - createFolderAndParents(packageFolder.getParent(), monitor.split(1)); - } - if (packageFolder.exists() && !packageFolder.isLinked()) { - packageFolder.delete(true, monitor.split(1)); - } - packageFolder.createLink( - javaInfo.getBazelPackage().getLocation(), - IResource.REPLACE, - monitor.split(1)); - - // remove all files not created as part of this loop - deleteAllFilesNotInFolderList(virtualSourceFolder, packageFolder, monitor.split(1)); - - // done + // check for existing folder + sourceFolder = project.getProject().getFolder(dir); + if (sourceFolder.exists() && !sourceFolder.isLinked()) { + // check if there is any linked parent we can remove + var parent = sourceFolder.getParent(); + while ((parent != null) && (parent.getType() != IResource.PROJECT)) { + if (parent.isLinked()) { + parent.delete(true, monitor.split(1)); break; } + parent = parent.getParent(); + } + if (sourceFolder.exists()) { + // TODO create problem marker + LOG.warn( + "Impossible to support project '{}' - found existing source directoy which cannot be deleted!", + project); + continue NEXT_FOLDER; + } + } - // check for existing folder - sourceFolder = project.getProject().getFolder(dir); - if (sourceFolder.exists() && !sourceFolder.isLinked()) { - // check if there is any linked parent we can remove - var parent = sourceFolder.getParent(); - while ((parent != null) && (parent.getType() != IResource.PROJECT)) { - if (parent.isLinked()) { - parent.delete(true, monitor.split(1)); - break; - } - parent = parent.getParent(); - } - if (sourceFolder.exists()) { - // TODO create problem marker - LOG.warn( - "Impossible to support project '{}' - found existing source directoy which cannot be deleted!", - project); - continue NEXT_FOLDER; - } - } + // ensure the parent exists + if (!sourceFolder.getParent().exists()) { + createFolderAndParents(sourceFolder.getParent(), monitor.split(1)); + } - // ensure the parent exists - if (!sourceFolder.getParent().exists()) { - createFolderAndParents(sourceFolder.getParent(), monitor.split(1)); - } + // create link to folder + sourceFolder.createLink( + javaInfo.getBazelPackage().getLocation().append(dir), + IResource.REPLACE, + monitor.split(1)); + } + } - // create link to folder - sourceFolder.createLink( - javaInfo.getBazelPackage().getLocation().append(dir), - IResource.REPLACE, - monitor.split(1)); - } + private void linkSourceFilesWithoutCommonRoot(JavaSourceInfo sourceInfo, IFolder virtualSourceFolder, + SubMonitor monitor) throws CoreException { + if (!sourceInfo.hasSourceFilesWithoutCommonRoot()) { + return; + } + + // create the "srcs" folder + createFolderAndParents(virtualSourceFolder, monitor.split(1)); + // build emulated Java package structure and link files + var files = sourceInfo.getSourceFilesWithoutCommonRoot(); + Set linkedFiles = new HashSet<>(); + for (JavaSourceEntry fileEntry : files) { + // peek at Java package to find proper "root" + var packagePath = fileEntry.getDetectedPackagePath(); + var packageFolder = virtualSourceFolder.getFolder(packagePath); + if (!packageFolder.exists()) { + createFolderAndParents(packageFolder, monitor.split(1)); } + // create link to file + var file = packageFolder.getFile(fileEntry.getPath().lastSegment()); + file.createLink(fileEntry.getLocation(), IResource.REPLACE, monitor.split(1)); + + // remember for cleanup + linkedFiles.add(file); + } + + // remove all files not created as part of this loop + deleteAllFilesNotInAllowList(virtualSourceFolder, linkedFiles, monitor.split(1)); + } + + /** + * Creates Eclipse virtual files/folders for sources collected in the {@link JavaSourceInfo}. + * + * @param project + * @param sourceInfo + * @param progress + * @throws CoreException + */ + protected void linkSourcesIntoProject(BazelProject project, JavaProjectInfo javaInfo, IProgressMonitor progress) + throws CoreException { + var monitor = SubMonitor.convert(progress, 100); + try { + linkSourceFilesWithoutCommonRoot( + javaInfo.getSourceInfo(), + getFileSystemMapper().getVirtualSourceFolder(project), + monitor); + linkSourceDirectories( + project, + javaInfo, + javaInfo.getSourceInfo(), + getFileSystemMapper().getVirtualSourceFolder(project), + getFileSystemMapper().getGeneratedSourcesFolder(project), + monitor); + + linkSourceFilesWithoutCommonRoot( + javaInfo.getSourceInfo(), + getFileSystemMapper().getVirtualSourceFolderForTests(project), + monitor); + linkSourceDirectories( + project, + javaInfo, + javaInfo.getTestSourceInfo(), + getFileSystemMapper().getVirtualSourceFolderForTests(project), + getFileSystemMapper().getGeneratedSourcesFolderForTests(project), + monitor); + // ensure the BUILD file is linked var buildFileLocation = javaInfo.getBazelPackage().getBuildFileLocation(); var buildFile = project.getProject().getFile(buildFileLocation.lastSegment()); diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BuildFileAndVisibilityDrivenProvisioningStrategy.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BuildFileAndVisibilityDrivenProvisioningStrategy.java index 381a208e..7cd609da 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BuildFileAndVisibilityDrivenProvisioningStrategy.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BuildFileAndVisibilityDrivenProvisioningStrategy.java @@ -471,6 +471,9 @@ protected List doProvisionProjects(Collection targets .collect(joining(", "))))); } + // configure links + linkGeneratedSourcesIntoProject(project, javaInfo, monitor.split(1)); + // configure classpath configureRawClasspath(project, javaInfo, monitor.split(1)); diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerPackageProvisioningStrategy.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerPackageProvisioningStrategy.java index 63ba559b..7120788f 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerPackageProvisioningStrategy.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerPackageProvisioningStrategy.java @@ -256,6 +256,9 @@ protected List doProvisionProjects(Collection targets .collect(joining(", "))))); } + // configure links + linkGeneratedSourcesIntoProject(project, javaInfo, monitor.split(1)); + // configure classpath configureRawClasspath(project, javaInfo, monitor.split(1)); 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 ede12905..a42dcb7a 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 @@ -197,6 +197,7 @@ protected BazelProject provisionJavaLibraryProject(BazelTarget target, SubMonito // configure links linkSourcesIntoProject(project, javaInfo, monitor.split(1)); + linkGeneratedSourcesIntoProject(project, javaInfo, monitor.split(1)); // configure classpath configureRawClasspath(project, javaInfo, monitor.split(1)); diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaProjectInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaProjectInfo.java index 17e1e1d0..df09616f 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaProjectInfo.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaProjectInfo.java @@ -179,13 +179,13 @@ private void addToSrc(Collection srcs, String srcFileOrLabel) throws Core public IStatus analyzeProjectRecommendations(IProgressMonitor monitor) throws CoreException { var result = new MultiStatus(JavaProjectInfo.class, 0, "Java Analysis Result"); - sourceInfo = new JavaSourceInfo(this.srcs, bazelPackage.getLocation()); + sourceInfo = new JavaSourceInfo(this.srcs, bazelPackage); sourceInfo.analyzeSourceDirectories(result); resourceInfo = new JavaResourceInfo(resources, bazelPackage); resourceInfo.analyzeResourceDirectories(result); - testSourceInfo = new JavaSourceInfo(this.testSrcs, bazelPackage.getLocation(), sourceInfo); + testSourceInfo = new JavaSourceInfo(this.testSrcs, bazelPackage, sourceInfo); testSourceInfo.analyzeSourceDirectories(result); testResourceInfo = new JavaResourceInfo(testResources, bazelPackage); diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceEntry.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceEntry.java index aa92c50d..ada9da65 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceEntry.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceEntry.java @@ -7,12 +7,12 @@ import org.eclipse.core.runtime.IPath; /** - * A source entry points to exactly one .java source file. It contains additional logic for extracting - * the package path from the location. + * A source entry points to exactly one .java source file. It contains additional logic for extracting the + * package path from the location. */ public class JavaSourceEntry implements Entry { - private static boolean endsWith(IPath path, IPath lastSegments) { + static boolean endsWith(IPath path, IPath lastSegments) { if (path.segmentCount() < lastSegments.segmentCount()) { return false; } @@ -60,6 +60,13 @@ public boolean equals(Object obj) { && Objects.equals(relativePath, other.relativePath); } + /** + * @return the Bazel package location this entry is contained in + */ + public IPath getBazelPackageLocation() { + return bazelPackageLocation; + } + /** * {@return absolute location of of the container of this path entry} */ @@ -93,8 +100,8 @@ public IPath getPathParent() { } /** - * @return first few segments of {@link #getPathParent()} which could be the source directory, or - * null if unlikely + * @return first few segments of {@link #getPathParent()} (relative to {@link #getBazelPackageLocation()}) which + * could be the source directory, or null if unlikely */ public IPath getPotentialSourceDirectoryRoot() { var detectedPackagePath = getDetectedPackagePath(); @@ -113,6 +120,14 @@ public int hashCode() { return Objects.hash(bazelPackageLocation, relativePath); } + /** + * {@return true if the entry is generated, i.e. located outside a Bazel package, false + * otherwise} + */ + public boolean isExternalOrGenerated() { + return false; + } + @Override public String toString() { return relativePath + " (relativePathParent=" + relativePathParent + ", bazelPackageLocation=" diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceInfo.java index 9c8eda94..42ab9e0f 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceInfo.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSourceInfo.java @@ -1,18 +1,27 @@ package com.salesforce.bazel.eclipse.core.model.discovery.projects; import static java.lang.String.format; +import static java.nio.file.Files.copy; +import static java.nio.file.Files.createDirectories; import static java.nio.file.Files.find; import static java.nio.file.Files.isRegularFile; import static java.nio.file.Files.readString; +import static java.nio.file.Files.walkFileTree; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; import java.io.IOException; import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -21,16 +30,22 @@ import java.util.Set; import java.util.StringTokenizer; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.MultiStatus; -import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.core.ToolFactory; import org.eclipse.jdt.core.compiler.ITerminalSymbols; import org.eclipse.jdt.core.compiler.InvalidInputException; +import com.salesforce.bazel.eclipse.core.model.BazelPackage; +import com.salesforce.bazel.eclipse.core.model.BazelTarget; +import com.salesforce.bazel.eclipse.core.model.BazelWorkspace; + /** * Source information used by {@link JavaProjectInfo} to analyze the srcs information in order to identify * root directories or split packages and recommend a layout. @@ -38,7 +53,7 @@ public class JavaSourceInfo { private static final IPath NOT_FOLLOWING_JAVA_PACKAGE_STRUCTURE = - new Path("_not_following_java_package_structure_"); + IPath.forPosix("_not_following_java_package_structure_"); private static boolean isJavaFile(java.nio.file.Path file) { return isRegularFile(file) && file.getFileName().toString().endsWith(".java"); @@ -48,6 +63,7 @@ private static boolean isJavaFile(java.nio.file.Path file) { private final Collection srcs; private final IPath bazelPackageLocation; private final JavaSourceInfo sharedSourceInfo; + private final BazelWorkspace bazelWorkspace; /** * A list of all source files impossible to identify a common root directory @@ -60,10 +76,11 @@ private static boolean isJavaFile(java.nio.file.Path file) { */ private Map sourceDirectoriesWithFilesOrGlobs; - public JavaSourceInfo(Collection srcs, IPath bazelPackageLocation) { + public JavaSourceInfo(Collection srcs, BazelPackage bazelPackage) { this.srcs = srcs; - this.bazelPackageLocation = bazelPackageLocation; + this.bazelPackageLocation = bazelPackage.getLocation(); this.sharedSourceInfo = null; + this.bazelWorkspace = bazelPackage.getBazelWorkspace(); } /** @@ -79,19 +96,20 @@ public JavaSourceInfo(Collection srcs, IPath bazelPackageLocation) { * @param bazelPackageLocation * @param sharedSourceInfo */ - public JavaSourceInfo(Collection srcs, IPath bazelPackageLocation, JavaSourceInfo sharedSourceInfo) { + public JavaSourceInfo(Collection srcs, BazelPackage bazelPackage, JavaSourceInfo sharedSourceInfo) { this.srcs = srcs; - this.bazelPackageLocation = bazelPackageLocation; + this.bazelPackageLocation = bazelPackage.getLocation(); this.sharedSourceInfo = sharedSourceInfo; + this.bazelWorkspace = bazelPackage.getBazelWorkspace(); } @SuppressWarnings("unchecked") public void analyzeSourceDirectories(MultiStatus result) throws CoreException { // build an index of all source files and their parent directories (does not need to maintain order) - Map> sourceEntriesByParentFolder = new HashMap<>(); + Map> sourceEntriesByParentFolder = new HashMap<>(); // group by potential source roots - Function groupingByPotentialSourceRoots = fileEntry -> { + Function groupingByPotentialSourceRoots = fileEntry -> { // detect package if necessary if (fileEntry.detectedPackagePath == null) { fileEntry.detectedPackagePath = detectPackagePath(fileEntry); @@ -109,37 +127,44 @@ public void analyzeSourceDirectories(MultiStatus result) throws CoreException { return NOT_FOLLOWING_JAVA_PACKAGE_STRUCTURE; } - // build second index of parent for all entries with a potential source root - // this is needed in order to identify split packages later - sourceEntriesByParentFolder.putIfAbsent(fileEntry.getPathParent(), new ArrayList<>()); - sourceEntriesByParentFolder.get(fileEntry.getPathParent()).add(fileEntry); - - // return the potential source root (relative) - return potentialSourceDirectoryRoot.makeRelative().removeTrailingSeparator(); + // return the potential source root + return potentialSourceDirectoryRoot; }; // collect the potential list of source directories var sourceEntriesBySourceRoot = new LinkedHashMap(); - for (Entry srcEntry : srcs) { - if (srcEntry instanceof JavaSourceEntry javaSourceFile) { - var sourceDirectory = groupingByPotentialSourceRoots.apply(javaSourceFile); - if (!sourceEntriesBySourceRoot.containsKey(sourceDirectory)) { - var list = new ArrayList<>(); + + // define a function for JavaSourceEntry so we can reuse it for sources and extracted srcjars + Function javaSourceEntryCollector = javaSourceFile -> { + var sourceDirectory = groupingByPotentialSourceRoots.apply(javaSourceFile); + if (!sourceEntriesBySourceRoot.containsKey(sourceDirectory)) { + var list = new ArrayList<>(); + list.add(javaSourceFile); + sourceEntriesBySourceRoot.put(sourceDirectory, list); + } else { + var maybeList = sourceEntriesBySourceRoot.get(sourceDirectory); + if (maybeList instanceof List list) { list.add(javaSourceFile); - sourceEntriesBySourceRoot.put(sourceDirectory, list); } else { - var maybeList = sourceEntriesBySourceRoot.get(sourceDirectory); - if (maybeList instanceof List list) { - list.add(javaSourceFile); - } else { - result.add( - Status.error( - format( - "It looks like source root '%s' is already mapped to a glob pattern. Please split into a separate targets. We cannot support this in the IDE.", - sourceDirectory))); - } + result.add( + Status.error( + format( + "It looks like source root '%s' is already mapped to a glob pattern. Please split into a separate targets. We cannot support this in the IDE.", + sourceDirectory))); } + } + return null; // not relevant + }; + + for (Entry srcEntry : srcs) { + if (srcEntry instanceof JavaSourceEntry javaSourceFile) { + javaSourceEntryCollector.apply(javaSourceFile); + + // build second index of parent for all entries with a potential source root + // this is needed in order to identify split packages (in same directory) later + sourceEntriesByParentFolder.putIfAbsent(javaSourceFile.getPathParent(), new ArrayList<>()); + sourceEntriesByParentFolder.get(javaSourceFile.getPathParent()).add(javaSourceFile); } else if (srcEntry instanceof GlobEntry globEntry) { if (sourceEntriesByParentFolder.containsKey(globEntry.getRelativeDirectoryPath())) { result.add( @@ -150,20 +175,25 @@ public void analyzeSourceDirectories(MultiStatus result) throws CoreException { } else { sourceEntriesBySourceRoot.put(globEntry.getRelativeDirectoryPath(), globEntry); } + } else if (srcEntry instanceof LabelEntry labelEntry) { + var bazelTarget = bazelWorkspace.getBazelTarget(labelEntry.getLabel()); + var srcJars = bazelTarget.getRuleOutput() + .stream() + .filter(p -> "srcjar".equals(p.getFileExtension())) + .collect(toList()); + for (IPath srcjar : srcJars) { + var srcjarFolder = extractSrcJar(bazelTarget, srcjar); + collectJavaSourcesInFolder(srcjarFolder).forEach(javaSourceEntryCollector::apply); + } } else { - // check if the source has label dependencies - result.add( - Status.warning( - format( - "Found source label reference '%s'. The project may not be fully supported in the IDE.", - srcEntry))); + throw new CoreException(Status.error(format("Unexpected source '%s'!", srcEntry))); } } // discover folders that contain more .java files then declared in srcs // (this is a strong split-package indication) Set potentialSplitPackageOrSubsetFolders = new HashSet<>(); - for (Map.Entry> entry : sourceEntriesByParentFolder.entrySet()) { + for (Map.Entry> entry : sourceEntriesByParentFolder.entrySet()) { var potentialSourceRoot = entry.getKey(); if (isContainedInSharedSourceDirectories(potentialSourceRoot)) { // don't check for split packages for stuff covered in shared sources already @@ -208,6 +238,10 @@ public void analyzeSourceDirectories(MultiStatus result) throws CoreException { // don't check for split packages for stuff covered in shared sources already continue; } + if (potentialSourceRoot.isAbsolute()) { + // don't check for split packages in srcjars (generated code) + continue; + } var potentialSourceRootPath = bazelPackageLocation.append(potentialSourceRoot).toPath(); try { @@ -231,7 +265,8 @@ public void analyzeSourceDirectories(MultiStatus result) throws CoreException { } } - // don't issue split packages warning nfor stuff covered in shared sources already + // don't issue split packages warning for stuff covered in shared sources already + // (test code is allowed to have same package) if ((sharedSourceInfo != null) && sharedSourceInfo.hasSourceDirectories()) { potentialSplitPackageOrSubsetFolders.removeIf(this::isContainedInSharedSourceDirectories); } @@ -257,6 +292,25 @@ public void analyzeSourceDirectories(MultiStatus result) throws CoreException { } } + private Collection collectJavaSourcesInFolder(IPath directory) throws CoreException { + try { + List result = new ArrayList<>(); + walkFileTree(directory.toPath(), new SimpleFileVisitor<>() { + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + var path = IPath.fromPath(file); + if ("java".equals(path.getFileExtension())) { + result.add(new JavaSrcJarEntry(path.removeFirstSegments(directory.segmentCount()), directory)); + } + return FileVisitResult.CONTINUE; + } + }); + return result; + } catch (IOException e) { + throw new CoreException( + Status.error(format("Unable to scan directory '%s' for .java source files.", directory), e)); + } + } + private IPath detectPackagePath(JavaSourceEntry fileEntry) { // we inspect at most one file per directory (anything else is too weird to support) var previouslyDetectedPackagePath = detectedPackagePathsByFileEntryPathParent.get(fileEntry.getPathParent()); @@ -265,7 +319,7 @@ private IPath detectPackagePath(JavaSourceEntry fileEntry) { } // assume empty by default - IPath packagePath = Path.EMPTY; + var packagePath = IPath.EMPTY; var packageName = readPackageName(fileEntry); if (packageName.length() > 0) { var packageNameSegments = new StringTokenizer(new String(packageName), "."); @@ -280,6 +334,50 @@ private IPath detectPackagePath(JavaSourceEntry fileEntry) { return packagePath; } + /** + * Extract the source jar (typically found in the bazel-bin directory of the package) into a directory for + * consumption as source folder in an Eclipse project. + *

+ * The srcjar will be extracted into a directory inside bazel-bin. + *

+ * + * @param bazelTarget + * the target producing the source jar + * @param srcjar + * the path to the source jar + * @return absolute file system path to the directory containing the extracted sources + * @throws CoreException + */ + private IPath extractSrcJar(BazelTarget bazelTarget, IPath srcjar) throws CoreException { + var jarFile = bazelWorkspace.getBazelBinLocation() + .append(bazelTarget.getBazelPackage().getWorkspaceRelativePath()) + .append(srcjar); + var targetDirectory = jarFile.removeLastSegments(1).append("_eclipse").append(srcjar.lastSegment()); + + // TODO: need a more sensitive delta/diff (including removal) for Eclipse resource refresh + + var destination = targetDirectory.toPath(); + try (var archive = new ZipFile(jarFile.toFile())) { + // sort entries by name to always create folders first + List entries = + archive.stream().sorted(Comparator.comparing(ZipEntry::getName)).collect(toList()); + for (ZipEntry entry : entries) { + var entryDest = destination.resolve(entry.getName()); + if (entry.isDirectory()) { + createDirectories(entryDest); + } else { + try (var is = archive.getInputStream(entry)) { + copy(is, entryDest, StandardCopyOption.REPLACE_EXISTING); + } + } + } + } catch (IOException e) { + throw new CoreException(Status.error(format("Error extracting srcjar '%s'", srcjar), e)); + } + + return targetDirectory; + } + public IPath getBazelPackageLocation() { return bazelPackageLocation; } @@ -330,7 +428,7 @@ public IPath[] getExclutionPatternsForSourceDirectory(IPath sourceDirectory) { if (excludePatterns != null) { var exclusionPatterns = new IPath[excludePatterns.size()]; for (var i = 0; i < exclusionPatterns.length; i++) { - exclusionPatterns[i] = Path.forPosix(excludePatterns.get(i)); + exclusionPatterns[i] = IPath.forPosix(excludePatterns.get(i)); } return exclusionPatterns; } @@ -355,7 +453,7 @@ public IPath[] getInclusionPatternsForSourceDirectory(IPath sourceDirectory) { if (includePatterns != null) { var exclusionPatterns = new IPath[includePatterns.size()]; for (var i = 0; i < exclusionPatterns.length; i++) { - exclusionPatterns[i] = Path.forPosix(includePatterns.get(i)); + exclusionPatterns[i] = IPath.forPosix(includePatterns.get(i)); } return exclusionPatterns; } @@ -401,6 +499,29 @@ private boolean isContainedInSharedSourceDirectories(IPath potentialSourceRoot) || sharedSourceDirectories.stream().anyMatch(p -> p.isPrefixOf(potentialSourceRoot)); } + /** + * Performs a check of all entries discovered for a source directory to match a given predicate. + * + * @param sourceDirectory + * the source directory (must be contained in {@link #getSourceDirectories()}) + * @param predicate + * the predicate that all entries for the specified source directory must match + * @return true if all match, false otherwise + */ + public boolean matchAllSourceDirectoryEntries(IPath sourceDirectory, Predicate predicate) { + var fileOrGlob = requireNonNull( + requireNonNull(sourceDirectoriesWithFilesOrGlobs, "no source directories discovered").get(sourceDirectory), + () -> format("source directory '%s' unknown", sourceDirectory)); + if (fileOrGlob instanceof GlobEntry globEntry) { + return predicate.test(globEntry); + } + if (fileOrGlob instanceof List listOfEntries) { + // the case is save assuming no programming mistakes in this class + return listOfEntries.stream().map(JavaSourceEntry.class::cast).allMatch(predicate); + } + return false; + } + @SuppressWarnings("deprecation") // use of TokenNameIdentifier is ok here private String readPackageName(JavaSourceEntry fileEntry) { var packageName = new StringBuilder(); diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSrcJarEntry.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSrcJarEntry.java new file mode 100644 index 00000000..2ffc4e55 --- /dev/null +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/JavaSrcJarEntry.java @@ -0,0 +1,70 @@ +/*- + * Copyright (c) 2023 Salesforce and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Salesforce - adapted from M2E, JDT or other Eclipse project + */ +package com.salesforce.bazel.eclipse.core.model.discovery.projects; + +import org.eclipse.core.runtime.IPath; + +/** + * An entry inside a .srcjar. + *

+ * In contrast to a standard {@link JavaSourceEntry} this specialization overrides the contract of + * {@link #getPotentialSourceDirectoryRoot()} to be an absolute path to a directory somewhere on the file system. + * Additional, the {@link #getBazelPackageLocation()} is also not really a Bazel package but the directory of the + * explosed srcjar. + *

+ */ +public class JavaSrcJarEntry extends JavaSourceEntry { + + public JavaSrcJarEntry(IPath relativePath, IPath srcJarDirectory) { + super(relativePath, srcJarDirectory); + } + + /** + * {@retun the location to the exploded srcjar} + */ + @Override + public IPath getBazelPackageLocation() { + return super.getBazelPackageLocation(); + } + + /** + * @return first few segments of {@link #getPathParent()} (within {@link #getSrcJarDirectoryLocation()}) which could + * be the source directory, or null if unlikely + */ + @Override + public IPath getPotentialSourceDirectoryRoot() { + var detectedPackagePath = getDetectedPackagePath(); + + // note, we check the full path because we *want* to identify files from targets defined within a Java package + var absolutePathToJavaFileDirectory = getSrcJarDirectoryLocation().append(getPathParent()); + if (endsWith(absolutePathToJavaFileDirectory, detectedPackagePath)) { + return absolutePathToJavaFileDirectory.removeLastSegments(detectedPackagePath.segmentCount()); + } + + return getSrcJarDirectoryLocation(); // assume srcjar directory + } + + /** + * {@retun the location to the exploded srcjar} + */ + public IPath getSrcJarDirectoryLocation() { + // the package location is the src jar directory + return getBazelPackageLocation(); + } + + @Override + public boolean isExternalOrGenerated() { + return true; + } +} diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/LabelEntry.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/LabelEntry.java index 0b3558b2..e2bbbd24 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/LabelEntry.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/projects/LabelEntry.java @@ -30,6 +30,13 @@ public boolean equals(Object obj) { return Objects.equals(label, other.label); } + /** + * @return the label + */ + public BazelLabel getLabel() { + return label; + } + @Override public int hashCode() { return Objects.hash(label);