From c3b3a293fe1b6c33cffb1b480b1f956470142966 Mon Sep 17 00:00:00 2001 From: Arthur McGibbon Date: Sat, 30 Jul 2022 23:15:21 +0100 Subject: [PATCH] Support scala android plugin on Gradle --- .../gradle/model/BloopConverter.scala | 156 ++++++++++-------- .../gradle/tasks/BloopInstallTask.scala | 4 +- .../gradle/ConfigGenerationSuite.scala | 147 ++++++++++++++++- 3 files changed, 230 insertions(+), 77 deletions(-) diff --git a/integrations/gradle-bloop/src/main/scala/bloop/integrations/gradle/model/BloopConverter.scala b/integrations/gradle-bloop/src/main/scala/bloop/integrations/gradle/model/BloopConverter.scala index 4189b82efc..ba2c29fbfe 100644 --- a/integrations/gradle-bloop/src/main/scala/bloop/integrations/gradle/model/BloopConverter.scala +++ b/integrations/gradle-bloop/src/main/scala/bloop/integrations/gradle/model/BloopConverter.scala @@ -65,7 +65,7 @@ import org.gradle.plugins.ide.internal.tooling.java.DefaultInstalledJdk * Define the conversion from Gradle's project model to Bloop's project model. * @param parameters Plugin input parameters */ -class BloopConverter(parameters: BloopParameters, info: String => Unit) { +class BloopConverter(parameters: BloopParameters) { def toBloopConfig( projectName: String, @@ -174,9 +174,10 @@ class BloopConverter(parameters: BloopParameters, info: String => Unit) { // some configs aren't allowed to be resolved - hence the catch // this can bring too many artifacts into the resolution section (e.g. junit on main projects) but there's no way to know which artifact is required by which sourceset // filter out internal scala plugin configurations - val additionalModules = project.getConfigurations.asScala + val allArtifacts = project.getConfigurations.asScala .filter(_.isCanBeResolved) .flatMap(getConfigurationArtifacts) + val additionalModules = allArtifacts .filterNot(f => allOutputsToSourceSets.contains(f.getFile)) .map(artifactToConfigModule(_, project)) .toList @@ -195,27 +196,34 @@ class BloopConverter(parameters: BloopParameters, info: String => Unit) { (List(Tag.Test), Some(Config.Test.defaultConfiguration)) else (List(Tag.Library), None) - val bloopProject = Config.Project( - name = projectName, - directory = project.getProjectDir.toPath, - workspaceDir = Option(project.getRootProject.getProjectDir.toPath), - sources = sources, - sourcesGlobs = None, - sourceRoots = None, - dependencies = projectDependencies, - classpath = compileClasspathItems, - out = outDir, - classesDir = classesDir, - resources = if (resources.isEmpty) None else Some(resources), - `scala` = None, - java = getAndroidJavaConfig(project, variant), - sbt = None, - test = testConfig, - platform = None, - resolution = if (modules.isEmpty) None else Some(Config.Resolution(modules)), - tags = if (tags.isEmpty) None else Some(tags) - ) - Success(Config.File(Config.File.LatestVersion, bloopProject)) + for { + scalaConfig <- getScalaConfig( + project, + None, + allArtifacts + ) + + bloopProject = Config.Project( + name = projectName, + directory = project.getProjectDir.toPath, + workspaceDir = Option(project.getRootProject.getProjectDir.toPath), + sources = sources, + sourcesGlobs = None, + sourceRoots = None, + dependencies = projectDependencies, + classpath = compileClasspathItems, + out = outDir, + classesDir = classesDir, + resources = if (resources.isEmpty) None else Some(resources), + `scala` = scalaConfig, + java = getAndroidJavaConfig(variant), + sbt = None, + test = testConfig, + platform = None, + resolution = if (modules.isEmpty) None else Some(Config.Resolution(modules)), + tags = if (tags.isEmpty) None else Some(tags) + ) + } yield Config.File(Config.File.LatestVersion, bloopProject) } } @@ -229,13 +237,11 @@ class BloopConverter(parameters: BloopParameters, info: String => Unit) { * * NOTE: Java classes will be also put into the above defined directory, not as with Gradle * - * @param projectName unique project name * @param project The Gradle project model * @param sourceSet The source set to convert * @return Bloop configuration */ def toBloopConfig( - projectName: String, project: Project, sourceSet: SourceSet, targetDir: File @@ -355,7 +361,7 @@ class BloopConverter(parameters: BloopParameters, info: String => Unit) { for { scalaConfig <- getScalaConfig( project, - sourceSet, + Some(sourceSet), compileArtifacts ) @@ -672,12 +678,12 @@ class BloopConverter(parameters: BloopParameters, info: String => Unit) { Option(variant.getJavaCompileProvider().getOrNull) } - private def getAndroidJavaConfig(project: Project, variant: BaseVariant): Option[Config.Java] = { + private def getAndroidJavaConfig(variant: BaseVariant): Option[Config.Java] = { getAndroidJavaCompile(variant).flatMap(javaCompile => { val options = javaCompile.getOptions // bug in DefaultJavaCompileSpec handling Android bootstrapClasspath causes crash so set to null options.setBootstrapClasspath(null) - getJavaConfig(project, javaCompile, options) + getJavaConfig(javaCompile, options) }) } @@ -812,9 +818,6 @@ class BloopConverter(parameters: BloopParameters, info: String => Unit) { private def getOutDir(targetDir: File, projectName: String): Path = (targetDir / projectName / "build").toPath - private def getOutDir(targetDir: File, project: Project, sourceSet: SourceSet): Path = - getOutDir(targetDir, getProjectName(project, sourceSet)) - private def getClassesDir(targetDir: File, projectName: String): Path = (targetDir / projectName / "build" / "classes").toPath @@ -903,12 +906,14 @@ class BloopConverter(parameters: BloopParameters, info: String => Unit) { private def getScalaConfig( project: Project, - sourceSet: SourceSet, - artifacts: List[ResolvedArtifactResult] + sourceSet: Option[SourceSet], + artifacts: Iterable[ResolvedArtifactResult] ): Try[Option[Config.Scala]] = { def isJavaOnly: Boolean = { - val allSourceFiles = sourceSet.getAllSource.getFiles.asScala.toList - !allSourceFiles.filter(f => f.exists && f.isFile).exists(_.getName.endsWith(".scala")) + !sourceSet.exists(ss => { + val allSourceFiles = ss.getAllSource.getFiles.asScala.toList + allSourceFiles.filter(f => f.exists && f.isFile).exists(_.getName.endsWith(".scala")) + }) } // Finding the compiler group and version from the standard Scala library added as dependency @@ -922,41 +927,57 @@ class BloopConverter(parameters: BloopParameters, info: String => Unit) { artifactIds.filter(f => stdLibNames.contains(f.getComponentIdentifier.getModule)) stdLibIds.headOption match { case Some(stdLibArtifact) => - val scalaCompileTaskName = sourceSet.getCompileTaskName("scala") - val scalaCompileTask = project.getTask[ScalaCompile](scalaCompileTaskName) - - if (scalaCompileTask != null) { - val scalaVersion = stdLibArtifact.getComponentIdentifier.getVersion - val scalaOrg = stdLibArtifact.getComponentIdentifier.getGroup - val scalaJars = scalaCompileTask.getScalaClasspath.asScala.map(_.toPath).toList - val opts = scalaCompileTask.getScalaCompileOptions - val options = optionList(opts) ++ getPluginsAsOptions(scalaCompileTask) - val compilerName = parameters.compilerName.getOrElse("scala-compiler") - val compileOrder = - if (!sourceSet.getJava.getSourceDirectories.isEmpty) JavaThenScala - else Mixed - val setup = CompileSetup.empty.copy(order = compileOrder) - - // Use the compile setup and analysis out defaults, Gradle doesn't expose its customization - Success( - Some( - Config - .Scala(scalaOrg, compilerName, scalaVersion, options, scalaJars, None, Some(setup)) - ) - ) - } else { - if (isJavaOnly) Success(None) - else { - // This is a heavy error on Gradle's side, but we will only report it in Scala projects - Failure( - new GradleException(s"$scalaCompileTaskName task is missing from ${project.getName}") + val scalaCompileTask = sourceSet + .map(sourceSet => { + // task name is defined on the source set + val scalaCompileTaskName = sourceSet.getCompileTaskName("scala") + Option(project.getTask[ScalaCompile](scalaCompileTaskName)) + }) + .getOrElse({ + // no sourceset - probably Android plugin - look for any ScalaCompile task + val scalaCompileTasks = project.getTasks.withType(classOf[ScalaCompile]) + scalaCompileTasks.asScala.headOption + }) + + scalaCompileTask match { + case Some(compileTask) => + val scalaVersion = stdLibArtifact.getComponentIdentifier.getVersion + val scalaOrg = stdLibArtifact.getComponentIdentifier.getGroup + val scalaJars = compileTask.getScalaClasspath.asScala.map(_.toPath).toList + val opts = compileTask.getScalaCompileOptions + val options = optionList(opts) ++ getPluginsAsOptions(compileTask) + val compilerName = parameters.compilerName.getOrElse("scala-compiler") + val noJavaFiles = + sourceSet.exists(sourceSet => sourceSet.getJava.getSourceDirectories.isEmpty) + val compileOrder = if (noJavaFiles) Mixed else JavaThenScala + val setup = CompileSetup.empty.copy(order = compileOrder) + + // Use the compile setup and analysis out defaults, Gradle doesn't expose its customization + Success( + Some( + Config + .Scala( + scalaOrg, + compilerName, + scalaVersion, + options, + scalaJars, + None, + Some(setup) + ) + ) ) - } + case None => + if (isJavaOnly) Success(None) + else { + // This is a heavy error on Gradle's side, but we will only report it in Scala projects + Failure(new GradleException(s"No ScalaCompile task in ${project.getName}")) + } } - case None if isJavaOnly => Success(None) case None => - val target = s"project ${project.getName}/${sourceSet.getName}" + val target = + s"project ${project.getName}/${sourceSet.map(sourceSet => sourceSet.getName).getOrElse("No sourceset")}" val artifactNames = if (artifacts.isEmpty) "" else @@ -988,11 +1009,10 @@ class BloopConverter(parameters: BloopParameters, info: String => Unit) { private def getJavaConfig(project: Project, sourceSet: SourceSet): Option[Config.Java] = { val javaCompile = getJavaCompileTask(project, sourceSet) val options = javaCompile.getOptions - getJavaConfig(project, javaCompile, options) + getJavaConfig(javaCompile, options) } private def getJavaConfig( - project: Project, javaCompile: JavaCompile, options: CompileOptions ): Option[Config.Java] = { diff --git a/integrations/gradle-bloop/src/main/scala/bloop/integrations/gradle/tasks/BloopInstallTask.scala b/integrations/gradle-bloop/src/main/scala/bloop/integrations/gradle/tasks/BloopInstallTask.scala index bc74e0bf63..ac21cde3cc 100644 --- a/integrations/gradle-bloop/src/main/scala/bloop/integrations/gradle/tasks/BloopInstallTask.scala +++ b/integrations/gradle-bloop/src/main/scala/bloop/integrations/gradle/tasks/BloopInstallTask.scala @@ -44,7 +44,7 @@ class BloopInstallTask extends DefaultTask with PluginUtils with TaskLogging { def runBloopPlugin(): Unit = { val parameters = extension.createParameters - val converter = new BloopConverter(parameters, info) + val converter = new BloopConverter(parameters) val targetDir: File = parameters.targetDir info(s"Generating Bloop configuration to ${targetDir.getAbsolutePath}") @@ -97,7 +97,7 @@ object ScalaJavaInstall { val targetFile = targetDir / s"$projectName.json" // Let's keep the error message as similar to the one in the sbt plugin as possible info(s"Generated ${targetFile.getAbsolutePath}") - converter.toBloopConfig(projectName, project, sourceSet, targetDir) match { + converter.toBloopConfig(project, sourceSet, targetDir) match { case Failure(reason) => info(s"Skipping ${project.getName}/${sourceSet.getName} because: $reason") case Success(bloopConfig) => diff --git a/integrations/gradle-bloop/src/test/scala/bloop/integrations/gradle/ConfigGenerationSuite.scala b/integrations/gradle-bloop/src/test/scala/bloop/integrations/gradle/ConfigGenerationSuite.scala index 5692d2b7b7..1526cef8ff 100644 --- a/integrations/gradle-bloop/src/test/scala/bloop/integrations/gradle/ConfigGenerationSuite.scala +++ b/integrations/gradle-bloop/src/test/scala/bloop/integrations/gradle/ConfigGenerationSuite.scala @@ -33,11 +33,8 @@ import org.junit._ import org.junit.rules.TemporaryFolder /* - * To remote debug the ConfigGenerationSuite... - * - change "gradlePluginBuildSettings" "Keys.fork in Test" in BuildPlugin.scala to "false" - * - scala-library-2.12.8.jar disappears from classpath when fork=false. Add manually for now to getClasspath output - * - add ".withDebug(true)" to the GradleRunner in the particular test to debug and ".forwardOutput()" to see println - * - run tests under gradleBloop212 project + * To debug the ConfigGenerationSuite within VSCode... + * - add `.withDebug(true)` to the GradleRunner in the particular test to debug and `.forwardOutput()` see println */ // minimum supported version @@ -47,15 +44,23 @@ class ConfigGenerationSuite_5_0 extends ConfigGenerationSuite { } // maximum supported version -class ConfigGenerationSuite_7_3 extends ConfigGenerationSuite { - protected val gradleVersion: String = "7.3" +class ConfigGenerationSuite_7_5 extends ConfigGenerationSuite { + protected val gradleVersion: String = "7.5" protected val supportsCurrentJavaVersion: Boolean = true } +/* +// needed for scala android plugin testing - Disabled - see #worksWithAndroidScalaPlugin +class ConfigGenerationSuite_Android_Scala_plugin extends ConfigGenerationSuite { + protected val gradleVersion: String = "6.6" + protected val supportsCurrentJavaVersion: Boolean = !Properties.isJavaAtLeast("15") +} + */ abstract class ConfigGenerationSuite extends BaseConfigSuite { protected val gradleVersion: String protected val supportsCurrentJavaVersion: Boolean private def supportsAndroid: Boolean = gradleVersion >= "6.1.1" + //private def supportsAndroidScalaPlugin: Boolean = gradleVersion == "6.6" private def supportsScala3: Boolean = gradleVersion >= "7.3" private def canConsumeTestRuntime: Boolean = gradleVersion < "7.0" private def supportsLazyArchives: Boolean = gradleVersion >= "4.9" @@ -311,6 +316,134 @@ abstract class ConfigGenerationSuite extends BaseConfigSuite { } } +// This test should run but won't. +// It works as a local gradle build file. +// Leaving this example commented as it's the only test for the scala-android plugin. +// The issue is that the scala-android plugin requires the Android plugin to be applied first and that doesn't seem to be happening when using the GradleRunner but does work if this is run as a build file. + + /* + @Test def worksWithAndroidScalaPlugin: Unit = { + if (supportsAndroid && supportsAndroidScalaPlugin && supportsCurrentJavaVersion) { + val buildSettings = testProjectDir.newFile("settings.gradle") + testProjectDir.newFolder("src", "main", "scala") + testProjectDir.newFolder("src", "main", "java") + testProjectDir.newFolder("src", "androidTest", "scala") + testProjectDir.newFolder("src", "androidTest", "java") + val buildFile = testProjectDir.newFile("build.gradle") + + writeBuildScript( + buildFile, + s""" + |buildscript { + | repositories { + | google() + | jcenter() + | } + | dependencies { + | classpath 'com.android.tools.build:gradle:4.0.2' + | classpath 'scala.android.plugin:scala-android-plugin:20210222.1057' + | } + |} + |plugins { + | id 'bloop' + |} + | + |allprojects { + | apply plugin: 'com.android.application' + | apply plugin: 'com.android.internal.application' + | apply plugin: 'bloop' + | + | android { + | compileSdkVersion 29 + | } + |} + | + |project.plugins.withId("com.android.internal.application") { + | // delay plugin apply til after Android + | allprojects { + | apply plugin: "scala.android" + | } + |} + | + |tasks.withType(ScalaCompile) { + | scalaCompileOptions.additionalParameters = ["-deprecation", "-unchecked", "-encoding", "utf8"] + |} + | + |repositories { + | mavenCentral() + |} + |dependencies { + | implementation 'org.scala-lang:scala-library:2.13.8' + |} + """.stripMargin + ) + + writeBuildScript( + buildSettings, + """ + |rootProject.name = 'android-project' + """.stripMargin + ) + + createHelloWorldScalaTestSource(testProjectDir.getRoot, "") + + GradleRunner + .create() + .withGradleVersion(gradleVersion) + .withProjectDir(testProjectDir.getRoot) + .withPluginClasspath(getClasspath) + .withDebug(true) + .forwardOutput() + .withArguments("bloopInstall", "-Si") + .build() + + val bloopDir = new File(testProjectDir.getRoot, ".bloop") + + val bloopDebug = new File(bloopDir, "android-project-debug.json") + val bloopDebugAndroidTest = new File(bloopDir, "android-project-debug-androidTest.json") + val bloopRelease = new File(bloopDir, "android-project-release.json") + + val configDebug = readValidBloopConfig(bloopDebug) + val configDebugAndroidTest = readValidBloopConfig(bloopDebugAndroidTest) + val configRelease = readValidBloopConfig(bloopRelease) + + assert(hasTag(configDebug, Tag.Library)) + assert(hasTag(configDebugAndroidTest, Tag.Test)) + assert(hasTag(configRelease, Tag.Library)) + + assert(configDebug.project.dependencies.isEmpty) + assertEquals(List("android-project-debug"), configDebugAndroidTest.project.dependencies.sorted) + assert(configRelease.project.dependencies.isEmpty) + + assert(hasCompileClasspathEntryName(configDebugAndroidTest, "/android-project-debug/build/classes")) + + assert(configDebug.project.test.isEmpty) + assert(configDebugAndroidTest.project.test.nonEmpty) + assert(configRelease.project.test.isEmpty) + + assertTrue(hasCompileClasspathEntryName(configDebug, "/R.jar")) + assertTrue(hasCompileClasspathEntryName(configDebugAndroidTest, "/R.jar")) + assertTrue(hasCompileClasspathEntryName(configRelease, "/R.jar")) + + assertTrue(configDebug.project.`scala`.nonEmpty) + assertTrue(configDebugAndroidTest.project.`scala`.nonEmpty) + assertTrue(configRelease.project.`scala`.nonEmpty) + + assertEquals( + List("-deprecation", "-encoding", "utf8", "-unchecked"), + configDebug.project.`scala`.get.options + ) + assertEquals( + List("-deprecation", "-encoding", "utf8", "-unchecked"), + configDebugAndroidTest.project.`scala`.get.options + ) + assertEquals( + List("-deprecation", "-encoding", "utf8", "-unchecked"), + configRelease.project.`scala`.get.options + ) + } + } + */ @Test def worksWithSourcesSetSourceNotEqualToResources(): Unit = { if (supportsCurrentJavaVersion) { val buildFile = testProjectDir.newFile("build.gradle")