diff --git a/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSets.java b/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSets.java index b6ad63bd..af1dec73 100644 --- a/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSets.java +++ b/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSets.java @@ -10,5 +10,5 @@ * List of all Gradle source set instances. */ public interface GradleSourceSets extends Serializable { - public List getGradleSourceSets(); + List getGradleSourceSets(); } diff --git a/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSetsMetadata.java b/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSetsMetadata.java new file mode 100644 index 00000000..00435b27 --- /dev/null +++ b/model/src/main/java/com/microsoft/java/bs/gradle/model/GradleSourceSetsMetadata.java @@ -0,0 +1,45 @@ +package com.microsoft.java.bs.gradle.model; + +import java.io.File; +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * Provides necessary information about Gradle source sets, + * enabling mapping of dependencies between them. + */ +public interface GradleSourceSetsMetadata extends Serializable { + + /** + * Returns a map that associates each Gradle source set with its corresponding + * classpath files in a gradle project. This typically includes any libraries + * or dependencies required for compilation within that source set. + * + *

+ * The keys of the map represent instances of the {@link GradleSourceSet} class, + * identifying all the source sets within the project. + *

+ *

+ * The values of the map are lists of {@link File} objects, representing the + * classpath files associated with the corresponding source set. + *

+ */ + Map> getGradleSourceSetsToClasspath(); + + /** + * Returns a map that associates output files with the Gradle source sets that + * generated them. This is useful for understanding the origin of generated artifacts. + * + *

+ * The keys of the map are {@link File} objects, representing individual + * output files produced during the build process. + *

+ *

+ * The values of the map are instances of the {@link GradleSourceSet} class, + * indicating the source set that generated the corresponding output file. + *

+ */ + Map getOutputsToSourceSet(); + +} diff --git a/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSets.java b/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSets.java index 31771c8d..93f00431 100644 --- a/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSets.java +++ b/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSets.java @@ -3,13 +3,13 @@ package com.microsoft.java.bs.gradle.model.impl; +import com.microsoft.java.bs.gradle.model.GradleSourceSet; +import com.microsoft.java.bs.gradle.model.GradleSourceSets; + import java.util.List; import java.util.Objects; import java.util.stream.Collectors; -import com.microsoft.java.bs.gradle.model.GradleSourceSet; -import com.microsoft.java.bs.gradle.model.GradleSourceSets; - /** * Default implementation of {@link GradleSourceSets}. */ @@ -26,10 +26,12 @@ public DefaultGradleSourceSets(List gradleSourceSets) { * Copy constructor. */ public DefaultGradleSourceSets(GradleSourceSets sourceSets) { - this.gradleSourceSets = sourceSets.getGradleSourceSets().stream() - .map(DefaultGradleSourceSet::new).collect(Collectors.toList()); + this(sourceSets.getGradleSourceSets().stream() + .map(DefaultGradleSourceSet::new) + .collect(Collectors.toList())); } + @Override public List getGradleSourceSets() { return gradleSourceSets; } diff --git a/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSetsMetadata.java b/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSetsMetadata.java new file mode 100644 index 00000000..21459e6f --- /dev/null +++ b/model/src/main/java/com/microsoft/java/bs/gradle/model/impl/DefaultGradleSourceSetsMetadata.java @@ -0,0 +1,62 @@ +package com.microsoft.java.bs.gradle.model.impl; + +import com.microsoft.java.bs.gradle.model.GradleSourceSet; +import com.microsoft.java.bs.gradle.model.GradleSourceSetsMetadata; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Default implementation of {@link DefaultGradleSourceSetsMetadata}. + */ +public class DefaultGradleSourceSetsMetadata implements GradleSourceSetsMetadata { + + private Map> sourceSetsToClasspath; + private Map outputsToSourceSet; + + public DefaultGradleSourceSetsMetadata( + Map> sourceSetsToClasspath, + Map outputsToSourceSet + ) { + this.sourceSetsToClasspath = sourceSetsToClasspath; + this.outputsToSourceSet = outputsToSourceSet; + } + + @Override + public Map> getGradleSourceSetsToClasspath() { + return sourceSetsToClasspath; + } + + public void setSourceSetsToClasspath(Map> sourceSetsToClasspath) { + this.sourceSetsToClasspath = sourceSetsToClasspath; + } + + @Override + public Map getOutputsToSourceSet() { + return outputsToSourceSet; + } + + public void setOutputsToSourceSet(Map outputsToSourceSet) { + this.outputsToSourceSet = outputsToSourceSet; + } + + @Override + public int hashCode() { + return Objects.hash(sourceSetsToClasspath, outputsToSourceSet); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + DefaultGradleSourceSetsMetadata that = (DefaultGradleSourceSetsMetadata) obj; + return Objects.equals(sourceSetsToClasspath, that.sourceSetsToClasspath) + && Objects.equals(outputsToSourceSet, that.outputsToSourceSet); + } +} diff --git a/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/SourceSetsModelBuilder.java b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/SourceSetsModelBuilder.java index a7037120..bf458e16 100644 --- a/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/SourceSetsModelBuilder.java +++ b/plugin/src/main/java/com/microsoft/java/bs/gradle/plugin/SourceSetsModelBuilder.java @@ -13,6 +13,8 @@ import java.util.Map; import java.util.Set; +import com.microsoft.java.bs.gradle.model.GradleSourceSet; +import com.microsoft.java.bs.gradle.model.GradleSourceSetsMetadata; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.CopySpec; @@ -29,12 +31,9 @@ import org.gradle.tooling.provider.model.ToolingModelBuilder; import org.gradle.util.GradleVersion; -import com.microsoft.java.bs.gradle.model.BuildTargetDependency; -import com.microsoft.java.bs.gradle.model.GradleSourceSets; -import com.microsoft.java.bs.gradle.model.LanguageExtension; -import com.microsoft.java.bs.gradle.model.impl.DefaultBuildTargetDependency; import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSet; -import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSets; +import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSetsMetadata; +import com.microsoft.java.bs.gradle.model.LanguageExtension; import com.microsoft.java.bs.gradle.plugin.dependency.DependencyCollector; /** @@ -43,11 +42,15 @@ public class SourceSetsModelBuilder implements ToolingModelBuilder { @Override public boolean canBuild(String modelName) { - return modelName.equals(GradleSourceSets.class.getName()); + return modelName.equals(GradleSourceSetsMetadata.class.getName()); } @Override public Object buildAll(String modelName, Project rootProject) { + + Map> sourceSetsToClasspath = new HashMap<>(); + Map outputsToSourceSet = new HashMap<>(); + Set allProject = rootProject.getAllprojects(); SourceSetCache cache = new SourceSetCache(); // this set is used to eliminate the source, resource and output @@ -63,6 +66,7 @@ public Object buildAll(String modelName, Project rootProject) { DefaultGradleSourceSet gradleSourceSet = new DefaultGradleSourceSet(); cache.addGradleSourceSet(sourceSet, gradleSourceSet); cache.addProject(sourceSet, project); + gradleSourceSet.setBuildTargetDependencies(new HashSet<>()); gradleSourceSet.setGradleVersion(project.getGradle().getGradleVersion()); gradleSourceSet.setProjectName(project.getName()); String projectPath = project.getPath(); @@ -108,10 +112,13 @@ public Object buildAll(String modelName, Project rootProject) { List compileClasspath = new LinkedList<>(sourceSet.getCompileClasspath().getFiles()); gradleSourceSet.setCompileClasspath(compileClasspath); + sourceSetsToClasspath.put(gradleSourceSet, compileClasspath); + // source output dir File sourceOutputDir = getSourceOutputDir(sourceSet); if (sourceOutputDir != null) { gradleSourceSet.setSourceOutputDir(sourceOutputDir); + outputsToSourceSet.put(sourceOutputDir, gradleSourceSet); exclusionFromDependencies.add(sourceOutputDir); } @@ -124,6 +131,7 @@ public Object buildAll(String modelName, Project rootProject) { File resourceOutputDir = sourceSet.getOutput().getResourcesDir(); if (resourceOutputDir != null) { gradleSourceSet.setResourceOutputDir(resourceOutputDir); + outputsToSourceSet.put(resourceOutputDir, gradleSourceSet); exclusionFromDependencies.add(resourceOutputDir); } @@ -153,9 +161,35 @@ public Object buildAll(String modelName, Project rootProject) { } } }); + + if (!sourceSets.isEmpty()) { + // get all archive tasks for this project and find the dirs that are included in the archive + TaskCollection archiveTasks = + project.getTasks().withType(AbstractArchiveTask.class); + for (AbstractArchiveTask archiveTask : archiveTasks) { + Set archiveSourcePaths = getArchiveSourcePaths(archiveTask.getRootSpec()); + for (Object sourcePath : archiveSourcePaths) { + sourceSets.forEach(sourceSet -> { + DefaultGradleSourceSet gradleSourceSet = cache.getGradleSourceSet(sourceSet); + if (gradleSourceSet == null) { + return; + } + + if (sourceSet.getOutput().equals(sourcePath)) { + File archiveFile; + if (GradleVersion.current().compareTo(GradleVersion.version("5.1")) >= 0) { + archiveFile = archiveTask.getArchiveFile().get().getAsFile(); + } else { + archiveFile = archiveTask.getArchivePath(); + } + outputsToSourceSet.put(archiveFile, gradleSourceSet); + } + }); + } + } + } } - setSourceSetDependencies(cache); setModuleDependencies(cache, exclusionFromDependencies); for (SourceSet sourceSet : cache.getAllSourceSets()) { @@ -172,7 +206,6 @@ public Object buildAll(String modelName, Project rootProject) { Map extensions = new HashMap<>(); for (LanguageModelBuilder languageModelBuilder : GradleBuildServerPlugin.SUPPORTED_LANGUAGE_BUILDERS) { - if (languageModelBuilder.appliesFor(project, sourceSet)) { LanguageExtension extension = languageModelBuilder.getExtensionsFor(project, sourceSet, gradleSourceSet.getModuleDependencies()); @@ -182,10 +215,9 @@ public Object buildAll(String modelName, Project rootProject) { } } gradleSourceSet.setExtensions(extensions); - } - return new DefaultGradleSourceSets(new LinkedList<>(cache.getAllGradleSourceSets())); + return new DefaultGradleSourceSetsMetadata(sourceSetsToClasspath, outputsToSourceSet); } private void setModuleDependencies(SourceSetCache cache, Set exclusionFromDependencies) { @@ -201,64 +233,6 @@ private void setModuleDependencies(SourceSetCache cache, Set exclusionFrom } } - private void setSourceSetDependencies(SourceSetCache cache) { - // map all output dirs to their source sets - Map outputsToSourceSet = new HashMap<>(); - for (DefaultGradleSourceSet sourceSet : cache.getAllGradleSourceSets()) { - if (sourceSet.getSourceOutputDir() != null) { - outputsToSourceSet.put(sourceSet.getSourceOutputDir(), sourceSet); - } - if (sourceSet.getResourceOutputDir() != null) { - outputsToSourceSet.put(sourceSet.getResourceOutputDir(), sourceSet); - } - } - - // map all output jars to their source sets - for (Project project : cache.getAllProjects()) { - SourceSetContainer sourceSets = getSourceSetContainer(project); - if (sourceSets == null || sourceSets.isEmpty()) { - continue; - } - - // get all archive tasks for this project and find the dirs that are included in the archive - TaskCollection archiveTasks = - project.getTasks().withType(AbstractArchiveTask.class); - for (AbstractArchiveTask archiveTask : archiveTasks) { - Set archiveSourcePaths = getArchiveSourcePaths(archiveTask.getRootSpec()); - for (Object sourcePath : archiveSourcePaths) { - sourceSets.forEach(sourceSet -> { - DefaultGradleSourceSet gradleSourceSet = cache.getGradleSourceSet(sourceSet); - if (gradleSourceSet == null) { - return; - } - - if (sourceSet.getOutput().equals(sourcePath)) { - File archiveFile; - if (GradleVersion.current().compareTo(GradleVersion.version("5.1")) >= 0) { - archiveFile = archiveTask.getArchiveFile().get().getAsFile(); - } else { - archiveFile = archiveTask.getArchivePath(); - } - outputsToSourceSet.put(archiveFile, gradleSourceSet); - } - }); - } - } - } - - // match any classpath entries to other project's output dirs/jars to create dependencies - for (SourceSet sourceSet : cache.getAllSourceSets()) { - Set dependencies = new HashSet<>(); - for (File file : sourceSet.getCompileClasspath()) { - DefaultGradleSourceSet otherSourceSet = outputsToSourceSet.get(file); - if (otherSourceSet != null) { - dependencies.add(new DefaultBuildTargetDependency(otherSourceSet)); - } - } - cache.getGradleSourceSet(sourceSet).setBuildTargetDependencies(dependencies); - } - } - private SourceSetContainer getSourceSetContainer(Project project) { if (GradleVersion.current().compareTo(GradleVersion.version("5.0")) >= 0) { SourceSetContainer sourceSetContainer = project.getExtensions() @@ -326,7 +300,7 @@ private File getSourceOutputDir(SourceSet sourceSet) { Method getOutputDirMethod = SourceDirectorySet.class.getMethod("getOutputDir"); return (File) getOutputDirMethod.invoke(sourceSet.getJava()); } catch (NoSuchMethodException | SecurityException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException e) { + | IllegalArgumentException | InvocationTargetException e) { // ignore } } else { @@ -365,7 +339,7 @@ private Set getArchiveSourcePaths(CopySpec copySpec) { } } } catch (NoSuchMethodException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException e) { + | IllegalArgumentException | InvocationTargetException e) { // cannot get archive information } } diff --git a/plugin/src/test/java/com/microsoft/java/bs/gradle/plugin/GradleBuildServerPluginTest.java b/plugin/src/test/java/com/microsoft/java/bs/gradle/plugin/GradleBuildServerPluginTest.java index 80f07c15..171063c1 100644 --- a/plugin/src/test/java/com/microsoft/java/bs/gradle/plugin/GradleBuildServerPluginTest.java +++ b/plugin/src/test/java/com/microsoft/java/bs/gradle/plugin/GradleBuildServerPluginTest.java @@ -16,9 +16,14 @@ import java.util.List; import java.util.function.Consumer; import java.util.stream.Stream; +import java.util.ArrayList; +import com.microsoft.java.bs.gradle.model.GradleSourceSet; +import com.microsoft.java.bs.gradle.model.GradleSourceSets; +import com.microsoft.java.bs.gradle.model.GradleSourceSetsMetadata; import com.microsoft.java.bs.gradle.model.JavaExtension; import com.microsoft.java.bs.gradle.model.ScalaExtension; +import com.microsoft.java.bs.gradle.model.SupportedLanguages; import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSets; import org.gradle.tooling.GradleConnector; import org.gradle.tooling.ModelBuilder; @@ -28,10 +33,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import com.microsoft.java.bs.gradle.model.GradleSourceSet; -import com.microsoft.java.bs.gradle.model.GradleSourceSets; -import com.microsoft.java.bs.gradle.model.SupportedLanguages; - class GradleBuildServerPluginTest { private static Path projectPath; @@ -46,7 +47,8 @@ static void beforeClass() { } private GradleSourceSets getGradleSourceSets(ProjectConnection connect) throws IOException { - ModelBuilder modelBuilder = connect.model(GradleSourceSets.class); + ModelBuilder modelBuilder = + connect.model(GradleSourceSetsMetadata.class); File initScript = PluginHelper.getInitScript(); modelBuilder .addArguments("--init-script", initScript.getAbsolutePath()) @@ -55,7 +57,9 @@ private GradleSourceSets getGradleSourceSets(ProjectConnection connect) throws I .addArguments("-Dorg.gradle.logging.level=quiet") .addJvmArguments("-Dbsp.gradle.supportedLanguages=" + String.join(",", SupportedLanguages.allBspNames)); - return new DefaultGradleSourceSets(modelBuilder.get()); + GradleSourceSetsMetadata sourceSetsMetadata = modelBuilder.get(); + return new DefaultGradleSourceSets( + new ArrayList<>(sourceSetsMetadata.getGradleSourceSetsToClasspath().keySet())); } private interface ConnectionConsumer { diff --git a/server/src/main/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnector.java b/server/src/main/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnector.java index 58f7b5ed..5f65c110 100644 --- a/server/src/main/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnector.java +++ b/server/src/main/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnector.java @@ -15,11 +15,13 @@ import java.util.Set; import org.apache.commons.lang3.exception.ExceptionUtils; +import com.microsoft.java.bs.core.internal.gradle.actions.GetSourceSetsAction; +import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSets; +import org.gradle.tooling.BuildActionExecuter; import org.gradle.tooling.BuildException; import org.gradle.tooling.BuildLauncher; import org.gradle.tooling.GradleConnectionException; import org.gradle.tooling.GradleConnector; -import org.gradle.tooling.ModelBuilder; import org.gradle.tooling.ProjectConnection; import org.gradle.tooling.TestLauncher; import org.gradle.tooling.events.OperationType; @@ -32,7 +34,6 @@ import com.microsoft.java.bs.core.internal.reporter.ProgressReporter; import com.microsoft.java.bs.core.internal.reporter.TestReportReporter; import com.microsoft.java.bs.gradle.model.GradleSourceSets; -import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSets; import ch.epfl.scala.bsp4j.BuildClient; import ch.epfl.scala.bsp4j.BuildTargetIdentifier; @@ -74,7 +75,7 @@ private String getGradleVersion(ProjectConnection connection) { * Get the source sets of the Gradle project. * * @param projectUri uri of the project - * @param client connection to BSP client + * @param client connection to BSP client * @return an instance of {@link GradleSourceSets} */ public GradleSourceSets getGradleSourceSets(URI projectUri, BuildClient client) { @@ -85,25 +86,22 @@ public GradleSourceSets getGradleSourceSets(URI projectUri, BuildClient client) ProgressReporter reporter = new DefaultProgressReporter(client); ByteArrayOutputStream errorOut = new ByteArrayOutputStream(); try (ProjectConnection connection = getGradleConnector(projectUri).connect(); - errorOut) { - ModelBuilder customModelBuilder = Utils.getModelBuilder( - connection, - preferenceManager.getPreferences(), - GradleSourceSets.class - ); - customModelBuilder.addProgressListener(reporter, - OperationType.FILE_DOWNLOAD, OperationType.PROJECT_CONFIGURATION) + errorOut) { + BuildActionExecuter buildExecutor + = connection.action(new GetSourceSetsAction()); + buildExecutor.addProgressListener(reporter, + OperationType.FILE_DOWNLOAD, OperationType.PROJECT_CONFIGURATION) .setStandardError(errorOut) .addArguments("--init-script", initScript.getAbsolutePath()); if (Boolean.getBoolean("bsp.plugin.debug.enabled")) { - customModelBuilder.addJvmArguments( + buildExecutor.addJvmArguments( "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"); } - customModelBuilder.addJvmArguments("-Dbsp.gradle.supportedLanguages=" + buildExecutor.addJvmArguments("-Dbsp.gradle.supportedLanguages=" + String.join(",", preferenceManager.getClientSupportedLanguages())); // since the model returned from Gradle TAPI is a wrapped object, here we re-construct it // via a copy constructor and return as a POJO. - return new DefaultGradleSourceSets(customModelBuilder.get()); + return new DefaultGradleSourceSets(buildExecutor.run()); } catch (GradleConnectionException | IllegalStateException | IOException e) { String summary = e.getMessage(); if (errorOut.size() > 0) { @@ -118,15 +116,15 @@ public GradleSourceSets getGradleSourceSets(URI projectUri, BuildClient client) * Request Gradle daemon to run the tasks. * * @param projectUri uri of the project - * @param reporter reporter on feedback from Gradle - * @param tasks tasks to run + * @param reporter reporter on feedback from Gradle + * @param tasks tasks to run */ public StatusCode runTasks(URI projectUri, ProgressReporter reporter, String... tasks) { // Don't issue a start progress update - the listener will pick that up automatically final ByteArrayOutputStream errorOut = new ByteArrayOutputStream(); StatusCode statusCode = StatusCode.OK; try (ProjectConnection connection = getGradleConnector(projectUri).connect(); - errorOut + errorOut ) { BuildLauncher launcher = Utils.getBuildLauncher(connection, preferenceManager.getPreferences()); diff --git a/server/src/main/java/com/microsoft/java/bs/core/internal/gradle/actions/GetSourceSetsAction.java b/server/src/main/java/com/microsoft/java/bs/core/internal/gradle/actions/GetSourceSetsAction.java new file mode 100644 index 00000000..cf6403ce --- /dev/null +++ b/server/src/main/java/com/microsoft/java/bs/core/internal/gradle/actions/GetSourceSetsAction.java @@ -0,0 +1,112 @@ +package com.microsoft.java.bs.core.internal.gradle.actions; + +import com.microsoft.java.bs.gradle.model.BuildTargetDependency; +import com.microsoft.java.bs.gradle.model.GradleSourceSet; +import com.microsoft.java.bs.gradle.model.GradleSourceSets; +import com.microsoft.java.bs.gradle.model.GradleSourceSetsMetadata; +import com.microsoft.java.bs.gradle.model.impl.DefaultBuildTargetDependency; +import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSet; +import com.microsoft.java.bs.gradle.model.impl.DefaultGradleSourceSets; +import org.gradle.tooling.BuildAction; +import org.gradle.tooling.BuildController; +import org.gradle.tooling.model.gradle.GradleBuild; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * {@link BuildAction} that retrieves {@link DefaultGradleSourceSet} from a Gradle build, + * handling both normal and composite builds. + */ +public class GetSourceSetsAction implements BuildAction { + + /** + * Executes the build action and retrieves source sets from the Gradle build. + * + * @return A {@link DefaultGradleSourceSets} object containing all retrieved source sets. + */ + @Override + public GradleSourceSets execute(BuildController buildController) { + var traversedProjects = new HashSet(); + var sourceSetToClasspath = new HashMap>(); + var outputsToSourceSet = new HashMap(); + + GradleBuild buildModel = buildController.getBuildModel(); + String rootProjectName = buildModel.getRootProject().getName(); + fetchModels(buildController, + buildModel, + traversedProjects, + sourceSetToClasspath, + outputsToSourceSet, + rootProjectName); + + // Add dependencies + var sourceSets = new ArrayList(); + for (var entry : sourceSetToClasspath.entrySet()) { + + var dependencies = new HashSet(); + for (File file : entry.getValue()) { + GradleSourceSet otherSourceSet = outputsToSourceSet.get(file); + if (otherSourceSet != null && !Objects.equals(entry.getKey(), otherSourceSet)) { + dependencies.add(new DefaultBuildTargetDependency(otherSourceSet)); + } + } + + DefaultGradleSourceSet sourceSet = new DefaultGradleSourceSet(entry.getKey()); + sourceSet.setBuildTargetDependencies(dependencies); + sourceSets.add(sourceSet); + + } + + return new DefaultGradleSourceSets(sourceSets); + } + + /** + * Fetches source sets from the provided Gradle build model and + * stores them in a map categorized by project name. + * + * @param buildController The Gradle build controller used to interact with the build. + * @param build The Gradle build model representing the current build. + * @param traversedProjects A set of traversed project names to avoid cyclic dependencies. + * @param sourceSetToClasspath A map that associates GradleSourceSet objects with their + * corresponding classpath files. + * @param outputsToSourceSet A map that associates output files with the GradleSourceSet + * they belong to. + * @param buildName The name of the root project in the build. + */ + private void fetchModels( + BuildController buildController, + GradleBuild build, + Set traversedProjects, + Map> sourceSetToClasspath, + Map outputsToSourceSet, + String buildName + ) { + if (traversedProjects.contains(buildName)) { + return; + } + GradleSourceSetsMetadata sourceSets = buildController + .findModel(build.getRootProject(), GradleSourceSetsMetadata.class); + + traversedProjects.add(buildName); + sourceSetToClasspath.putAll(sourceSets.getGradleSourceSetsToClasspath()); + outputsToSourceSet.putAll(sourceSets.getOutputsToSourceSet()); + + for (GradleBuild includedBuild : build.getIncludedBuilds()) { + String includedBuildName = includedBuild.getRootProject().getName(); + fetchModels(buildController, + includedBuild, + traversedProjects, + sourceSetToClasspath, + outputsToSourceSet, + includedBuildName); + } + } + +} diff --git a/server/src/test/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnectorTest.java b/server/src/test/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnectorTest.java index 15cf94f3..760bcc56 100644 --- a/server/src/test/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnectorTest.java +++ b/server/src/test/java/com/microsoft/java/bs/core/internal/gradle/GradleApiConnectorTest.java @@ -137,6 +137,38 @@ void testGetGradleHasTests() { assertFalse(findSourceSet(gradleSourceSets, "test-tag [testFixtures]").hasTests()); } + @Test + void testCompositeBuild1() { + File projectDir = projectPath.resolve("composite-build-1").toFile(); + PreferenceManager preferenceManager = new PreferenceManager(); + preferenceManager.setPreferences(new Preferences()); + GradleApiConnector connector = new GradleApiConnector(preferenceManager); + GradleSourceSets gradleSourceSets = connector.getGradleSourceSets(projectDir.toURI(), null); + assertEquals(4, gradleSourceSets.getGradleSourceSets().size()); + findSourceSet(gradleSourceSets, "projectA [main]"); + findSourceSet(gradleSourceSets, "projectA [test]"); + findSourceSet(gradleSourceSets, "projectB [main]"); + findSourceSet(gradleSourceSets, "projectB [test]"); + } + + @Test + void testCompositeBuild2() { + File projectDir = projectPath.resolve("composite-build-2").toFile(); + PreferenceManager preferenceManager = new PreferenceManager(); + preferenceManager.setPreferences(new Preferences()); + GradleApiConnector connector = new GradleApiConnector(preferenceManager); + GradleSourceSets gradleSourceSets = connector.getGradleSourceSets(projectDir.toURI(), null); + assertEquals(6, gradleSourceSets.getGradleSourceSets().size()); + findSourceSet(gradleSourceSets, "app [test]"); + findSourceSet(gradleSourceSets, "string-utils [test]"); + findSourceSet(gradleSourceSets, "number-utils [test]"); + GradleSourceSet mainApp = findSourceSet(gradleSourceSets, "app [main]"); + GradleSourceSet mainStringUtils = findSourceSet(gradleSourceSets, "string-utils [main]"); + GradleSourceSet mainNumberUtils = findSourceSet(gradleSourceSets, "number-utils [main]"); + assertHasBuildTargetDependency(mainApp, mainStringUtils); + assertHasBuildTargetDependency(mainApp, mainNumberUtils); + } + private void assertHasBuildTargetDependency(GradleSourceSet sourceSet, GradleSourceSet dependency) { boolean exists = sourceSet.getBuildTargetDependencies().stream() diff --git a/testProjects/composite-build-1/build.gradle b/testProjects/composite-build-1/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/testProjects/composite-build-1/projectA/build.gradle b/testProjects/composite-build-1/projectA/build.gradle new file mode 100644 index 00000000..bde068fc --- /dev/null +++ b/testProjects/composite-build-1/projectA/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'java' +} diff --git a/testProjects/composite-build-1/projectA/settings.gradle b/testProjects/composite-build-1/projectA/settings.gradle new file mode 100644 index 00000000..91314219 --- /dev/null +++ b/testProjects/composite-build-1/projectA/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'projectA' \ No newline at end of file diff --git a/testProjects/composite-build-1/projectB/build.gradle b/testProjects/composite-build-1/projectB/build.gradle new file mode 100644 index 00000000..bde068fc --- /dev/null +++ b/testProjects/composite-build-1/projectB/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'java' +} diff --git a/testProjects/composite-build-1/projectB/settings.gradle b/testProjects/composite-build-1/projectB/settings.gradle new file mode 100644 index 00000000..36447f1f --- /dev/null +++ b/testProjects/composite-build-1/projectB/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'projectB' \ No newline at end of file diff --git a/testProjects/composite-build-1/settings.gradle b/testProjects/composite-build-1/settings.gradle new file mode 100644 index 00000000..6a7980da --- /dev/null +++ b/testProjects/composite-build-1/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'composite-build-1' + +includeBuild 'projectA' +includeBuild 'projectB' \ No newline at end of file diff --git a/testProjects/composite-build-2/build.gradle b/testProjects/composite-build-2/build.gradle new file mode 100644 index 00000000..ba39c38f --- /dev/null +++ b/testProjects/composite-build-2/build.gradle @@ -0,0 +1,11 @@ +defaultTasks 'run' + +tasks.register('run') { + dependsOn gradle.includedBuild('my-app').task(':app:run') +} + +task checkAll { + dependsOn gradle.includedBuild('my-app').task(':app:check') + dependsOn gradle.includedBuild('my-utils').task(':number-utils:check') + dependsOn gradle.includedBuild('my-utils').task(':string-utils:check') +} diff --git a/testProjects/composite-build-2/my-app/app/build.gradle b/testProjects/composite-build-2/my-app/app/build.gradle new file mode 100644 index 00000000..0bf1f45f --- /dev/null +++ b/testProjects/composite-build-2/my-app/app/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'application' +} + +application { + mainClass = 'org.sample.myapp.Main' +} + +dependencies { + implementation 'org.sample:number-utils:1.0' + implementation 'org.sample:string-utils:1.0' +} + +group 'org.sample' +version '1.0' + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/testProjects/composite-build-2/my-app/app/src/main/java/org/sample/myapp/Main.java b/testProjects/composite-build-2/my-app/app/src/main/java/org/sample/myapp/Main.java new file mode 100644 index 00000000..062d8bf0 --- /dev/null +++ b/testProjects/composite-build-2/my-app/app/src/main/java/org/sample/myapp/Main.java @@ -0,0 +1,16 @@ +package org.sample.myapp; + +import org.sample.numberutils.Numbers; +import org.sample.stringutils.Strings; + +public class Main { + + public static void main(String... args) { + new Main().printAnswer(); + } + + public void printAnswer() { + String output = Strings.concat(" The answer is ", Numbers.add(19, 23)); + System.out.println(output); + } +} diff --git a/testProjects/composite-build-2/my-app/settings.gradle b/testProjects/composite-build-2/my-app/settings.gradle new file mode 100644 index 00000000..553a0170 --- /dev/null +++ b/testProjects/composite-build-2/my-app/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'my-app' + +include 'app' diff --git a/testProjects/composite-build-2/my-utils/number-utils/build.gradle b/testProjects/composite-build-2/my-utils/number-utils/build.gradle new file mode 100644 index 00000000..acc7834d --- /dev/null +++ b/testProjects/composite-build-2/my-utils/number-utils/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java-library' +} + +group 'org.sample' +version '1.0' + +repositories { + mavenCentral() +} diff --git a/testProjects/composite-build-2/my-utils/number-utils/src/main/java/org/sample/numberutils/Numbers.java b/testProjects/composite-build-2/my-utils/number-utils/src/main/java/org/sample/numberutils/Numbers.java new file mode 100644 index 00000000..d36210c3 --- /dev/null +++ b/testProjects/composite-build-2/my-utils/number-utils/src/main/java/org/sample/numberutils/Numbers.java @@ -0,0 +1,5 @@ +package org.sample.numberutils; + +public class Numbers { + public static int add(int left, int right) { return left + right; } +} diff --git a/testProjects/composite-build-2/my-utils/settings.gradle b/testProjects/composite-build-2/my-utils/settings.gradle new file mode 100644 index 00000000..50e0f268 --- /dev/null +++ b/testProjects/composite-build-2/my-utils/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'my-utils' + +include 'number-utils', 'string-utils' diff --git a/testProjects/composite-build-2/my-utils/string-utils/build.gradle b/testProjects/composite-build-2/my-utils/string-utils/build.gradle new file mode 100644 index 00000000..0e797e3b --- /dev/null +++ b/testProjects/composite-build-2/my-utils/string-utils/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java-library' +} + +group 'org.sample' +version '1.0' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.apache.commons:commons-lang3:3.4' +} diff --git a/testProjects/composite-build-2/my-utils/string-utils/src/main/java/org/sample/stringutils/Strings.java b/testProjects/composite-build-2/my-utils/string-utils/src/main/java/org/sample/stringutils/Strings.java new file mode 100644 index 00000000..db81d7e8 --- /dev/null +++ b/testProjects/composite-build-2/my-utils/string-utils/src/main/java/org/sample/stringutils/Strings.java @@ -0,0 +1,13 @@ +package org.sample.stringutils; + +import org.apache.commons.lang3.StringUtils; + +public class Strings { + public static String concat(Object left, Object right) { + return strip(left) + " " + strip(right); + } + + private static String strip(Object val) { + return StringUtils.strip(String.valueOf(val)); + } +} diff --git a/testProjects/composite-build-2/settings.gradle b/testProjects/composite-build-2/settings.gradle new file mode 100644 index 00000000..b3fd0a94 --- /dev/null +++ b/testProjects/composite-build-2/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'my-composite-2' + +includeBuild 'my-app' +includeBuild 'my-utils'