Skip to content

Commit

Permalink
Switch to build service for sharing resolved rules between projects
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielThomas committed Sep 16, 2021
1 parent 87aeb53 commit d64d2ef
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class AlignRulesMultiprojectSpec extends IntegrationSpec {
fork = false
rulesJsonFile = new File(projectDir, "${moduleName}.json")
buildFile << """\
subprojects {
allprojects {
${applyPlugin(ResolutionRulesPlugin)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,20 @@ class ResolutionRulesPluginSpec extends IntegrationSpec {
output.contains 'nebula.resolution-rules is using ruleset: rules.jar'
}

def 'dependencies task with configuration on demand'() {
def subproject = addSubproject("subprojectA")
new File(subproject, "build.gradle") << """
apply plugin: 'java'
apply plugin: 'nebula.resolution-rules'
""".stripIndent()

when:
def result = runTasksSuccessfully(':subprojectA:dependencies', '--configuration', 'compileClasspath', '-Dorg.gradle.configureondemand=true')

then:
result.standardOutput.contains("Configuration on demand is an incubating feature.")
}

def 'replace module'() {
given:
buildFile << """
Expand Down
168 changes: 106 additions & 62 deletions src/main/kotlin/nebula/plugin/resolutionrules/plugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,30 @@ import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ConfigurationContainer
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import java.io.File
import java.util.*
import java.io.Serializable
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import javax.inject.Inject

const val RESOLUTION_RULES_CONFIG_NAME = "resolutionRules"

class ResolutionRulesPlugin : Plugin<Project> {
private val logger: Logger = Logging.getLogger(ResolutionRulesPlugin::class.java)
private val NEBULA_RECOMMENDER_BOM_CONFIG_NAME: String = "nebulaRecommenderBom"
private lateinit var project: Project
private lateinit var configurations: ConfigurationContainer
private lateinit var extension: NebulaResolutionRulesExtension
private val ignoredConfigurationPrefixes = listOf(RESOLUTION_RULES_CONFIG_NAME, SPRING_VERSION_MANAGEMENT_CONFIG_NAME,
NEBULA_RECOMMENDER_BOM_CONFIG_NAME, SCALA_INCREMENTAL_ANALYSIS_CONFIGURATION_PREFIX, KTLINT_CONFIGURATION_PREFIX, REPOSITORY_CONTENT_DESCRIPTOR_CONFIGURATION_PREFIX)
private val ignoredConfigurationSuffixes = listOf(PMD_CONFIGURATION_SUFFIX)

companion object Constants {
companion object {
val Logger: Logger = Logging.getLogger(ResolutionRulesPlugin::class.java)

const val NEBULA_RECOMMENDER_BOM_CONFIG_NAME: String = "nebulaRecommenderBom"
const val SPRING_VERSION_MANAGEMENT_CONFIG_NAME = "versionManagement"
const val KTLINT_CONFIGURATION_PREFIX = "ktlint"
const val PMD_CONFIGURATION_SUFFIX = "PmdAuxClasspath"
Expand All @@ -59,18 +64,32 @@ class ResolutionRulesPlugin : Plugin<Project> {
override fun apply(project: Project) {
this.project = project
configurations = project.configurations
extension = project.extensions.create("nebulaResolutionRules", NebulaResolutionRulesExtension::class.java, project)
extension =
project.extensions.create("nebulaResolutionRules", NebulaResolutionRulesExtension::class.java, project)

project.onExecute {
if (isCoreAlignmentEnabled()) {
logger.info("${project.name}: coreAlignmentSupport feature enabled")
Logger.info("${project.name}: coreAlignmentSupport feature enabled")
}
}

val rootProject = project.rootProject
rootProject.configurations.maybeCreate(RESOLUTION_RULES_CONFIG_NAME)
val configuration = project.configurations.maybeCreate(RESOLUTION_RULES_CONFIG_NAME)
if (project != rootProject) {
configuration.isCanBeConsumed = false
val rootProjectDependency = project.dependencies.project(
mapOf("path" to rootProject.path, "configuration" to RESOLUTION_RULES_CONFIG_NAME)
)
configuration.withDependencies { dependencies ->
dependencies.add(rootProjectDependency)
}
}
if (rootProject.extensions.findByType(NebulaResolutionRulesExtension::class.java) == null) {
rootProject.extensions.create("nebulaResolutionRules", NebulaResolutionRulesExtension::class.java, rootProject)
rootProject.extensions.create(
"nebulaResolutionRules",
NebulaResolutionRulesExtension::class.java,
rootProject
)
}

project.configurations.all { config ->
Expand All @@ -86,7 +105,9 @@ class ResolutionRulesPlugin : Plugin<Project> {
project.onExecute {
val ruleSet = extension.ruleSet()
when {
config.state != Configuration.State.UNRESOLVED || config.getObservedState() != Configuration.State.UNRESOLVED -> logger.warn("Dependency resolution rules will not be applied to $config, it was resolved before the project was executed")
config.state != Configuration.State.UNRESOLVED || config.getObservedState() != Configuration.State.UNRESOLVED -> Logger.warn(
"Dependency resolution rules will not be applied to $config, it was resolved before the project was executed"
)
else -> {
ruleSet.dependencyRulesPartOne().forEach { rule ->
rule.apply(project, config, config.resolutionStrategy, extension)
Expand All @@ -102,7 +123,7 @@ class ResolutionRulesPlugin : Plugin<Project> {

config.onResolve {
if (!dependencyRulesApplied) {
logger.debug("Skipping resolve rules for $config - dependency rules have not been applied")
Logger.debug("Skipping resolve rules for $config - dependency rules have not been applied")
} else {
val ruleSet = extension.ruleSet()
ruleSet.resolveRules(isCoreAlignmentEnabled()).forEach { rule ->
Expand All @@ -123,48 +144,91 @@ class ResolutionRulesPlugin : Plugin<Project> {
}
}

open class NebulaResolutionRulesExtension @Inject constructor(private val project: Project) {
@Suppress("UnstableApiUsage")
abstract class NebulaResolutionRulesService : BuildService<NebulaResolutionRulesService.Params> {
companion object {
private val logger: Logger = Logging.getLogger(ResolutionRulesPlugin::class.java)
private val mapper = objectMapper()
}

var include = ArrayList<String>()
var optional = ArrayList<String>()
var exclude = ArrayList<String>()
var useCoreGradleAlignment = false
private val Logger: Logger = Logging.getLogger(NebulaResolutionRulesService::class.java)
private val Mapper = objectMapper()

fun registerService(project: Project): Provider<NebulaResolutionRulesService> {
return project.gradle.sharedServices.registerIfAbsent(
"nebulaResolutionRules",
NebulaResolutionRulesService::class.java
) { spec ->
val resolutionRules = resolveResolutionRules(project)
spec.parameters.getResolutionRules().set(ResolutionRules(resolutionRules))
}
}

private val rulesByFile by lazy {
check(project == project.rootProject) { "This should only be called on the root project extension" }
val configuration = project.configurations.getByName(RESOLUTION_RULES_CONFIG_NAME)
val files = project.copyConfiguration(configuration).resolve()
val rules = LinkedHashMap<String, RuleSet>()
for (file in files) {
val filename = file.name
logger.debug("nebula.resolution-rules uses: $filename")
if (filename.endsWith(ResolutionRulesPlugin.JSON_EXT)) {
rules.putRules(mapper.parseJsonFile(file))
} else if (filename.endsWith(ResolutionRulesPlugin.JAR_EXT) || filename.endsWith(ResolutionRulesPlugin.ZIP_EXT)) {
logger.info("nebula.resolution-rules is using ruleset: $filename")
ZipFile(file).use { zip ->
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
if (entry.name.endsWith(ResolutionRulesPlugin.JSON_EXT)) {
rules.putRules(mapper.parseJsonStream(zip, entry))
private fun resolveResolutionRules(project: Project): Map<String, RuleSet> {
val configuration = project.configurations.getByName(RESOLUTION_RULES_CONFIG_NAME)
val files = configuration.resolve()
val rules = LinkedHashMap<String, RuleSet>()
for (file in files) {
val filename = file.name
Logger.debug("nebula.resolution-rules uses: $filename")
if (filename.endsWith(ResolutionRulesPlugin.JSON_EXT)) {
rules.putRules(Mapper.parseJsonFile(file))
} else if (filename.endsWith(ResolutionRulesPlugin.JAR_EXT) || filename.endsWith(ResolutionRulesPlugin.ZIP_EXT)) {
Logger.info("nebula.resolution-rules is using ruleset: $filename")
ZipFile(file).use { zip ->
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
if (entry.name.endsWith(ResolutionRulesPlugin.JSON_EXT)) {
rules.putRules(Mapper.parseJsonStream(zip, entry))
}
}
}
} else {
Logger.debug("Unsupported rules file extension for $file")
}
} else {
logger.debug("Unsupported rules file extension for $file")
}
return rules
}

private fun MutableMap<String, RuleSet>.putRules(ruleSet: RuleSet) {
if (put(ruleSet.name!!, ruleSet) != null) {
Logger.info("Found rules with the same name. Overriding existing ruleset ${ruleSet.name}")
}
}
rules

private fun ruleSetName(filename: String) =
filename.substring(0, filename.lastIndexOf(ResolutionRulesPlugin.JSON_EXT))

private fun ObjectMapper.parseJsonFile(file: File): RuleSet {
val ruleSetName = ruleSetName(file.name)
Logger.debug("Using $ruleSetName (${file.name}) a dependency rules source")
return readValue<RuleSet>(file).withName(ruleSetName)
}

private fun ObjectMapper.parseJsonStream(zip: ZipFile, entry: ZipEntry): RuleSet {
val ruleSetName = ruleSetName(File(entry.name).name)
Logger.debug("Using $ruleSetName (${zip.name}) a dependency rules source")
return readValue<RuleSet>(zip.getInputStream(entry)).withName(ruleSetName)
}
}

interface Params : BuildServiceParameters {
fun getResolutionRules(): Property<ResolutionRules>
}

class ResolutionRules(val byFile: Map<String, RuleSet>) : Serializable
}

open class NebulaResolutionRulesExtension @Inject constructor(private val project: Project) {
var include = ArrayList<String>()
var optional = ArrayList<String>()
var exclude = ArrayList<String>()
var useCoreGradleAlignment = false

fun ruleSet(): RuleSet {
val extension = project.rootProject.extensions.getByType(NebulaResolutionRulesExtension::class.java)
return extension.rulesByFile.filterKeys { ruleSet ->
val service = NebulaResolutionRulesService.registerService(project).get()
@Suppress("UnstableApiUsage") val rulesByFile = service.parameters
.getResolutionRules()
.get()
.byFile
return rulesByFile.filterKeys { ruleSet ->
when {
ruleSet.startsWith(ResolutionRulesPlugin.OPTIONAL_PREFIX) -> {
val ruleSetWithoutPrefix = ruleSet.substring(ResolutionRulesPlugin.OPTIONAL_PREFIX.length)
Expand All @@ -175,24 +239,4 @@ open class NebulaResolutionRulesExtension @Inject constructor(private val projec
}
}.values.flatten()
}

private fun MutableMap<String, RuleSet>.putRules(ruleSet: RuleSet) {
if (put(ruleSet.name!!, ruleSet) != null) {
logger.info("Found rules with the same name. Overriding existing ruleset ${ruleSet.name}")
}
}

private fun ruleSetName(filename: String) = filename.substring(0, filename.lastIndexOf(ResolutionRulesPlugin.JSON_EXT))

private fun ObjectMapper.parseJsonFile(file: File): RuleSet {
val ruleSetName = ruleSetName(file.name)
logger.debug("Using $ruleSetName (${file.name}) a dependency rules source")
return readValue<RuleSet>(file).withName(ruleSetName)
}

private fun ObjectMapper.parseJsonStream(zip: ZipFile, entry: ZipEntry): RuleSet {
val ruleSetName = ruleSetName(File(entry.name).name)
logger.debug("Using $ruleSetName (${zip.name}) a dependency rules source")
return readValue<RuleSet>(zip.getInputStream(entry)).withName(ruleSetName)
}
}
25 changes: 9 additions & 16 deletions src/main/kotlin/nebula/plugin/resolutionrules/rules.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,12 @@ import org.gradle.api.artifacts.*
import org.gradle.api.artifacts.component.ModuleComponentSelector
import org.gradle.api.internal.artifacts.DefaultModuleIdentifier
import org.gradle.api.internal.artifacts.DefaultModuleVersionIdentifier
import org.gradle.api.internal.artifacts.dsl.ModuleVersionSelectorParsers
import org.gradle.api.internal.artifacts.ivyservice.dependencysubstitution.DefaultDependencySubstitutions
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.ExactVersionSelector
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionSelector
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import java.io.Serializable

interface Rule {
interface Rule : Serializable {
fun apply(
project: Project,
configuration: Configuration,
Expand Down Expand Up @@ -59,7 +56,7 @@ data class RuleSet(
val deny: List<DenyRule> = emptyList(),
val exclude: List<ExcludeRule> = emptyList(),
val align: List<AlignRule> = emptyList()
) {
) : Serializable {

fun dependencyRulesPartOne() =
listOf(replace, deny, exclude).flatten() + listOf(SubstituteRules(substitute), RejectRules(reject))
Expand Down Expand Up @@ -131,13 +128,9 @@ data class SubstituteRule(
val module: String, val with: String, override var ruleSet: String?,
override val reason: String, override val author: String, override val date: String
) : BasicRule, Serializable {
lateinit var substitutedVersionId: ModuleVersionIdentifier
lateinit var withComponentSelector: ModuleComponentSelector
private val versionSelector by lazy {
check(substitutedVersionId.version.isNotEmpty()) { "Version may not be empty" }
val version = substitutedVersionId.version
VersionWithSelector(version).asSelector()
}
@Transient lateinit var substitutedVersionId: ModuleVersionIdentifier
@Transient lateinit var withComponentSelector: ModuleComponentSelector
@Transient lateinit var versionSelector: VersionSelector

override fun apply(
project: Project,
Expand Down Expand Up @@ -169,7 +162,7 @@ class SubstituteRules(val rules: List<SubstituteRule>) : Rule {
).apply { isAccessible = true }
}

private lateinit var rulesById: Map<ModuleIdentifier, List<SubstituteRule>>
@Transient private lateinit var rulesById: Map<ModuleIdentifier, List<SubstituteRule>>

override fun apply(
project: Project,
Expand All @@ -187,6 +180,7 @@ class SubstituteRules(val rules: List<SubstituteRule>) : Rule {
throw SubstituteRuleMissingVersionException(rule.with, rule)
}
rule.withComponentSelector = withModule
rule.versionSelector = VersionWithSelector(rule.substitutedVersionId.version).asSelector()
}
rule
}.groupBy { it.substitutedVersionId.module }
Expand Down Expand Up @@ -232,7 +226,7 @@ data class RejectRule(
override val date: String
) : ModuleRule {
val moduleVersionId = module.toModuleVersionId()
lateinit var versionSelector: VersionSelector
@Transient lateinit var versionSelector: VersionSelector

init {
if (moduleVersionId.version.isNotEmpty()) {
Expand Down Expand Up @@ -313,7 +307,6 @@ data class ExcludeRule(
override val author: String,
override val date: String
) : ModuleRule {
private val logger: Logger = Logging.getLogger(ExcludeRule::class.java)
private val moduleId = module.toModuleId()

@Override
Expand All @@ -325,7 +318,7 @@ data class ExcludeRule(
) {
val message =
"excluded $moduleId and transitive dependencies for all dependencies of this configuration by rule $ruleSet"
logger.debug(message)
ResolutionRulesPlugin.Logger.debug(message)
// TODO: would like a core Gradle feature that accepts a reason
configuration.exclude(moduleId.group, moduleId.name)
resolutionStrategy.componentSelection.withModule(moduleId.toString()) { selection ->
Expand Down

0 comments on commit d64d2ef

Please sign in to comment.