Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a Bloop-specific Gradle project configuration #106

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
18 changes: 16 additions & 2 deletions src/main/scala/bloop/integrations/gradle/BloopParameters.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package bloop.integrations.gradle

import java.io.File
import java.util.ArrayList

import scala.collection.JavaConverters._

import bloop.integrations.gradle.syntax._

import org.gradle.api.Project
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
Expand All @@ -21,6 +25,7 @@ import org.gradle.api.tasks.Optional
* stdLibName = "scala-library" // or "scala3-library_3"
* includeSources = true
* includeJavaDoc = false
* extendUserConfigurations = ["myCustomMainConfig"]
* }
* }}}
*/
Expand Down Expand Up @@ -58,6 +63,13 @@ case class BloopParametersExtension(project: Project) {
@Input @Optional def getIncludeJavadoc: Property[java.lang.Boolean] =
includeJavadoc_

private val extendUserConfigurations_ : ListProperty[String] =
project.getObjects().listProperty(classOf[String])
extendUserConfigurations_.set(new ArrayList[String]())

@Input @Optional def getExtendUserConfigurations: ListProperty[String] =
extendUserConfigurations_

def createParameters: BloopParameters = {
val defaultTargetDir =
project.getRootProject.workspacePath.resolve(".bloop").toFile
Expand All @@ -66,7 +78,8 @@ case class BloopParametersExtension(project: Project) {
Option(compilerName_.getOrNull),
Option(stdLibName_.getOrNull),
includeSources_.get,
includeJavadoc_.get
includeJavadoc_.get,
extendUserConfigurations_.get.asScala.toList
)
}
}
Expand All @@ -76,5 +89,6 @@ case class BloopParameters(
compilerName: Option[String],
stdLibName: Option[String],
includeSources: Boolean,
includeJavadoc: Boolean
includeJavadoc: Boolean,
extendUserConfigurations: List[String]
)
114 changes: 114 additions & 0 deletions src/main/scala/bloop/integrations/gradle/BloopPlugin.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package bloop.integrations.gradle

import scala.collection.JavaConverters._

import bloop.integrations.gradle.syntax._
import bloop.integrations.gradle.tasks.BloopInstallTask
import bloop.integrations.gradle.tasks.ConfigureBloopInstallTask
import bloop.integrations.gradle.tasks.PluginUtils

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.tasks.SourceSet

/**
* Main entry point of the gradle bloop plugin.
Expand All @@ -26,8 +31,49 @@ final class BloopPlugin extends Plugin[Project] {
s"Applying bloop plugin to project ${project.getName}",
Seq.empty: _*
)

project.createExtension[BloopParametersExtension]("bloop", project)

project.afterEvaluate(
new org.gradle.api.Action[Project] {
def execute(project: Project): Unit = {

val bloopParams = project.getExtension[BloopParametersExtension].createParameters

if (PluginUtils.hasJavaScalaPlugin(project)) {
project.allSourceSets.foreach { sourceSet =>
val bloopConfigName = generateBloopConfigName(sourceSet)
val bloopConfig = createBloopConfigForSourceSet(bloopConfigName, project)
val compatibleConfigNames =
findCompatibleConfigNamesFromSourceSet(sourceSet) ++
bloopParams.extendUserConfigurations

extendCompatibleConfigurationAfterEvaluate(
project,
bloopConfig,
compatibleConfigNames,
Set.empty
)
}
}

if (PluginUtils.hasAndroidPlugin(project)) {
// In the Android world, we don't have source sets for each variant, so instead
// we create an Android-specific configuration that we can use to resolve all
// relevant artifacts in all variants (the empty extend configs)
val bloopAndroidConfig = createBloopConfigForSourceSet("bloopAndroidConfig", project)

extendCompatibleConfigurationAfterEvaluate(
project,
bloopAndroidConfig,
Set.empty,
incompatibleAndroidConfigurations
)
}
}
}
)

// Creates two tasks: one to configure the plugin and the other one to generate the config files
val configureBloopInstall =
project.createTask[ConfigureBloopInstallTask]("configureBloopInstall")
Expand All @@ -36,4 +82,72 @@ final class BloopPlugin extends Plugin[Project] {
bloopInstall.dependsOn(configureBloopInstall)
()
}

private def findCompatibleConfigNamesFromSourceSet(sourceSet: SourceSet): Set[String] = Set(
sourceSet.getApiConfigurationName(),
sourceSet.getImplementationConfigurationName(),
sourceSet.getCompileOnlyConfigurationName(),
// sourceSet.getCompileOnlyApiConfigurationName(),
sourceSet.getCompileClasspathConfigurationName(),
sourceSet.getRuntimeOnlyConfigurationName(),
sourceSet.getRuntimeClasspathConfigurationName(),
sourceSet.getRuntimeElementsConfigurationName(),
"scalaCompilerPlugins"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think only getCompileClasspathConfigurationName(), getRuntimeClasspathConfigurationName() and scalaCompilerPlugins is needed here as the others aren't resolvable. See tables

)

private[this] val incompatibleAndroidConfigurations = Set[String](
"incrementalScalaAnalysisElements",
"incrementalScalaAnalysisFormain",
"incrementalScalaAnalysisFortest",
"incrementalScalaAnalysisForintegTest",
"incrementalScalaAnalysisForjmh",
"zinc"
)

private def createBloopConfigForSourceSet(
bloopConfigName: String,
project: Project
): Configuration = {
val bloopConfig = project.getConfigurations().create(bloopConfigName)
bloopConfig.setDescription(
"A configuration for Bloop to be able to export artifacts in all other configurations."
)

// Make this configuration not visbile in dependencyInsight reports
bloopConfig.setVisible(false)
// Allow this configuration to be resolved
bloopConfig.setCanBeResolved(true)
// This configuration is not meant to be consumed by other projects
bloopConfig.setCanBeConsumed(false)

bloopConfig
}

/**
* Makes the input configuration extend valid compatible configurations.
* Note that if extendConfigNames is empty, all resolvable and non-whitelisted
* configurations will be extended automatically.
*/
private def extendCompatibleConfigurationAfterEvaluate(
project: Project,
bloopSourceSetConfig: Configuration,
compatibleConfigNames: Set[String],
incompatibleConfigNames: Set[String]
): Unit = {
// Use consumer instead of Scala closure because of Scala 2.11 compat
val extendConfigurationIfCompatible = new java.util.function.Consumer[Configuration] {
def accept(config: Configuration): Unit = {
if (
config != bloopSourceSetConfig &&
config.isCanBeResolved &&
(compatibleConfigNames.isEmpty || compatibleConfigNames.contains(config.getName())) &&
!incompatibleConfigNames.contains(config.getName())
) {
bloopSourceSetConfig.extendsFrom(config)
}
}
}

project.getConfigurations.forEach(extendConfigurationIfCompatible)
}
}
69 changes: 38 additions & 31 deletions src/main/scala/bloop/integrations/gradle/model/BloopConverter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.ArtifactView.ViewConfiguration
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.artifacts.result.ComponentArtifactsResult
import org.gradle.api.artifacts.result.ResolvedArtifactResult
import org.gradle.api.attributes.Attribute
Expand All @@ -51,6 +53,7 @@ import org.gradle.api.internal.tasks.compile.JavaCompilerArgumentsBuilder
import org.gradle.api.plugins.JavaApplication
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.specs.Spec
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.api.tasks.compile.CompileOptions
Expand Down Expand Up @@ -191,13 +194,13 @@ class BloopConverter(parameters: BloopParameters) {
targetDir
)

// get all configurations dependencies - these go into the resolutions as the user can create their own config dependencies (e.g. compiler plugin jar)
// 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 allArtifacts = project.getConfigurations.asScala
.filter(_.isCanBeResolved)
.flatMap(getConfigurationArtifacts)
// We obtain all the artifacts in this configuration, which has been
// configured to extend all valid resolvable configurations to obtain the
// artifact jars present in all of the variants in an Android project
val bloopAndroidConfig = project.getConfiguration("bloopAndroidConfig")
assert(bloopAndroidConfig != null, "Missing bloopAndroidConfig!")
val allArtifacts = getConfigurationArtifacts(bloopAndroidConfig)

val additionalModules = allArtifacts
.filterNot(f => allOutputsToSourceSets.contains(f.getFile))
.map(artifactToConfigModule(_, project))
Expand Down Expand Up @@ -351,21 +354,13 @@ class BloopConverter(parameters: BloopParameters) {
targetDir
)

// get all configurations dependencies - these go into the resolutions as the user can create their own config dependencies (e.g. compiler plugin jar)
// 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 modules = project.getConfigurations.asScala
.filter(_.isCanBeResolved)
.filter(c =>
!List(
"incrementalScalaAnalysisElements",
"incrementalScalaAnalysisFormain",
"incrementalScalaAnalysisFortest",
"zinc"
).contains(c.getName)
)
.flatMap(getConfigurationArtifacts)
// Each source set has a bloop config name that extends the most important
// source set configurations to properly retrieve all relevant artifact jars
val bloopConfigName = generateBloopConfigName(sourceSet)
val bloopConfig = project.getConfiguration(bloopConfigName)
assert(bloopConfig != null, s"Missing $bloopConfigName configuration in project!")

val modules = getConfigurationArtifacts(bloopConfig)
.filter(f =>
!allArchivesToSourceSets.contains(f.getFile) &&
!allOutputDirsToSourceSets.contains(f.getFile)
Expand Down Expand Up @@ -432,18 +427,30 @@ class BloopConverter(parameters: BloopParameters) {
// get only jar artifacts
val artifactType = Attribute.of("artifactType", classOf[String])
val attributeType = "jar"

configuration.getIncoming
.artifactView(new Action[ViewConfiguration] {
override def execute(viewConfig: ViewConfiguration): Unit = {
viewConfig.setLenient(true)
viewConfig.attributes(new Action[AttributeContainer] {
override def execute(
attributeContainer: AttributeContainer
): Unit = {
attributeContainer.attribute(artifactType, attributeType)
()
}
})
viewConfig
.lenient(true)
.componentFilter(new Spec[ComponentIdentifier] {
def isSatisfiedBy(id: ComponentIdentifier): Boolean = {
// Filter out project dependencies as we're not interested in them
// Furthermore, if there are any configuration transforms Gradle
// must check that the artifact jars are present, and if we depend
// on project dependencies then it'll fail because project
// dependencies don't have existing jars at the time of resolution
!(id.isInstanceOf[ProjectComponentIdentifier])
}
})
.attributes(new Action[AttributeContainer] {
override def execute(
attributeContainer: AttributeContainer
): Unit = {
attributeContainer.attribute(artifactType, attributeType)
()
}
})
()
}
})
Expand Down
8 changes: 8 additions & 0 deletions src/main/scala/bloop/integrations/gradle/syntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,12 @@ object syntax {
implicit class FileExtension(file: File) {
def /(child: String): File = new File(file, child)
}

def capitalize(x: String): String = Option(x) match {
case Some(s) if s.nonEmpty => s.head.toUpper + s.tail
case _ => x
}

def generateBloopConfigName(sourceSet: SourceSet): String =
s"bloop${capitalize(sourceSet.getName())}Config"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3634,6 +3634,10 @@ abstract class ConfigGenerationSuite extends BaseConfigSuite {
| scalaCompilerPlugin
|}
|
|bloop {
| extendUserConfigurations = [configurations.scalaCompilerPlugin.name]
|}
|
|dependencies {
| implementation 'org.scala-lang:scala-library:2.12.8'
| scalaCompilerPlugin "org.scalameta:semanticdb-scalac_2.12.8:4.1.9"
Expand Down