Skip to content

Commit

Permalink
Introduce a bloopConfig configuration by source set
Browse files Browse the repository at this point in the history
The previous solution worked, but it was not ideal because it introduced
the concept of a `bloopConfig` that had to extend all the configurations
in the project (even across different source sets, e.g. main, test,
integTest, etc.).

This is awkward because the plugin generates a bloop config per source
set. So, if we have one bloop config extending all configurations from
all source sets, and then depending on that when generating bloop config
per source set, we're running into a lot of repeated resolutions to
produce the necessary artifacts and the resolution config section
contains a lot more artifacts than it should (e.g. foo.json and
foo-test.json would contain the same set of artifacts). Also, in theory,
this approach could also cause Gradle to add the wrong artifact version
because it would need reconciling all artifact across all source sets
is more likely to produce different results. This is a theoretical
point, but it'd be nice to avoid it.

So, the fix to all of this is to make a `bloopConfig` configuration per
source set. For example, the main source set gets a `bloopConfigMain`
configuration that extends from some default configurations that we care
about for Java/Scala plugins.

(In the Android world, there are no sourcesets, but "variants" so we
fallback on the previous behavior and depend on all the sources for
`bloopConfigAndroid`, a config that exists only in Android projects and
that adds the artifacts of all the project variants -- this is the
behavior one expects, and in this case we extend from all the
configurations in the variants for backwards compatibility.)

To make the process more customizable for end users, I've also added a
new property in the bloop configuration that lets users specify the
extra configurations that should be extended per source set:

```
bloop {
  extendUserConfigurations = [configurations.myCustomConfig.name]
}
```

This way, users have control over the configurations that should also
contribute artifacts to the resolution section in the bloop config.
This is nice and better than the previous approach.
  • Loading branch information
jvican committed May 17, 2024
1 parent 149489f commit 3a179bd
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 50 deletions.
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,6 +1,7 @@
package bloop.integrations.gradle

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

import bloop.integrations.gradle.syntax._

Expand All @@ -9,6 +10,9 @@ import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional

import scala.collection.JavaConverters._
import org.gradle.api.provider.ListProperty

/**
* Project extension to configure Bloop.
*
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: 88 additions & 26 deletions src/main/scala/bloop/integrations/gradle/BloopPlugin.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package bloop.integrations.gradle

import bloop.integrations.gradle.syntax._
import bloop.integrations.gradle.tasks.PluginUtils
import bloop.integrations.gradle.tasks.BloopInstallTask
import bloop.integrations.gradle.tasks.ConfigureBloopInstallTask
import scala.collection.JavaConverters._

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

/**
Expand All @@ -27,21 +30,48 @@ final class BloopPlugin extends Plugin[Project] {
s"Applying bloop plugin to project ${project.getName}",
Seq.empty: _*
)

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

val bloopConfig = project.getConfigurations().create("bloopConfig")
bloopConfig.setDescription(
"A configuration for Bloop to be able to export artifacts in all other configurations."
)
project.afterEvaluate(
new org.gradle.api.Action[Project] {
def execute(project: Project): Unit = {

// 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)
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
)
}
}

extendCompatibleConfigurationAfterEvaluate(project, bloopConfig)
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 =
Expand All @@ -52,7 +82,19 @@ final class BloopPlugin extends Plugin[Project] {
()
}

private[this] val incompatibleConfigurations = Set[String](
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"
)

private[this] val incompatibleAndroidConfigurations = Set[String](
"incrementalScalaAnalysisElements",
"incrementalScalaAnalysisFormain",
"incrementalScalaAnalysisFortest",
Expand All @@ -61,30 +103,50 @@ final class BloopPlugin extends Plugin[Project] {
"zinc"
)

def extendCompatibleConfigurationAfterEvaluate(
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,
bloopConfig: Configuration
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 != bloopConfig && config.isCanBeResolved &&
!incompatibleConfigurations.contains(config.getName())
config != bloopSourceSetConfig &&
config.isCanBeResolved &&
(compatibleConfigNames.isEmpty || compatibleConfigNames.contains(config.getName())) &&
!incompatibleConfigNames.contains(config.getName())
) {
bloopConfig.extendsFrom(config)
bloopSourceSetConfig.extendsFrom(config)
}
}
}

// Important to do this after evaluation so Gradle can add all the necessary project configuration before we extend them
project.afterEvaluate(
// Use Action instead of Scala closure because of Scala 2.11 compat
new org.gradle.api.Action[Project] {
def execute(project: Project): Unit = {
project.getConfigurations.forEach(extendConfigurationIfCompatible)
}
}
)
project.getConfigurations.forEach(extendConfigurationIfCompatible)
}
}
33 changes: 12 additions & 21 deletions src/main/scala/bloop/integrations/gradle/model/BloopConverter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -191,22 +191,12 @@ class BloopConverter(parameters: BloopParameters) {
targetDir
)

val bloopConfig = project.getConfiguration("bloopConfig")
val allArtifacts = getConfigurationArtifacts(bloopConfig)

// 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 allArtifacts2 = project.getConfigurations.asScala
// .filter(_.isCanBeResolved)
// .flatMap(getConfigurationArtifacts)

// System.out.println(s"""
// |All artifacts (new): $allArtifacts
// |All artifacts (old): $allArtifacts2
// """.stripMargin)
// 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))
Expand Down Expand Up @@ -361,11 +351,12 @@ 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 bloopConfig = project.getConfiguration("bloopConfig")
// 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) &&
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 @@ -3632,7 +3632,10 @@ abstract class ConfigGenerationSuite extends BaseConfigSuite {
|
|configurations {
| scalaCompilerPlugin
| bloopConfig.extendsFrom(scalaCompilerPlugin)
|}
|
|bloop {
| extendUserConfigurations = [configurations.scalaCompilerPlugin.name]
|}
|
|dependencies {
Expand Down

0 comments on commit 3a179bd

Please sign in to comment.