Skip to content

Commit d64d2ef

Browse files
committed
Switch to build service for sharing resolved rules between projects
1 parent 87aeb53 commit d64d2ef

File tree

4 files changed

+130
-79
lines changed

4 files changed

+130
-79
lines changed

src/integTest/groovy/nebula/plugin/resolutionrules/AlignRulesMultiprojectSpec.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class AlignRulesMultiprojectSpec extends IntegrationSpec {
3434
fork = false
3535
rulesJsonFile = new File(projectDir, "${moduleName}.json")
3636
buildFile << """\
37-
subprojects {
37+
allprojects {
3838
${applyPlugin(ResolutionRulesPlugin)}
3939
4040

src/integTest/groovy/nebula/plugin/resolutionrules/ResolutionRulesPluginSpec.groovy

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,20 @@ class ResolutionRulesPluginSpec extends IntegrationSpec {
197197
output.contains 'nebula.resolution-rules is using ruleset: rules.jar'
198198
}
199199

200+
def 'dependencies task with configuration on demand'() {
201+
def subproject = addSubproject("subprojectA")
202+
new File(subproject, "build.gradle") << """
203+
apply plugin: 'java'
204+
apply plugin: 'nebula.resolution-rules'
205+
""".stripIndent()
206+
207+
when:
208+
def result = runTasksSuccessfully(':subprojectA:dependencies', '--configuration', 'compileClasspath', '-Dorg.gradle.configureondemand=true')
209+
210+
then:
211+
result.standardOutput.contains("Configuration on demand is an incubating feature.")
212+
}
213+
200214
def 'replace module'() {
201215
given:
202216
buildFile << """

src/main/kotlin/nebula/plugin/resolutionrules/plugin.kt

Lines changed: 106 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,30 @@ import org.gradle.api.artifacts.Configuration
2626
import org.gradle.api.artifacts.ConfigurationContainer
2727
import org.gradle.api.logging.Logger
2828
import org.gradle.api.logging.Logging
29+
import org.gradle.api.provider.Property
30+
import org.gradle.api.provider.Provider
31+
import org.gradle.api.services.BuildService
32+
import org.gradle.api.services.BuildServiceParameters
2933
import java.io.File
30-
import java.util.*
34+
import java.io.Serializable
3135
import java.util.zip.ZipEntry
3236
import java.util.zip.ZipFile
3337
import javax.inject.Inject
3438

3539
const val RESOLUTION_RULES_CONFIG_NAME = "resolutionRules"
3640

3741
class ResolutionRulesPlugin : Plugin<Project> {
38-
private val logger: Logger = Logging.getLogger(ResolutionRulesPlugin::class.java)
39-
private val NEBULA_RECOMMENDER_BOM_CONFIG_NAME: String = "nebulaRecommenderBom"
4042
private lateinit var project: Project
4143
private lateinit var configurations: ConfigurationContainer
4244
private lateinit var extension: NebulaResolutionRulesExtension
4345
private val ignoredConfigurationPrefixes = listOf(RESOLUTION_RULES_CONFIG_NAME, SPRING_VERSION_MANAGEMENT_CONFIG_NAME,
4446
NEBULA_RECOMMENDER_BOM_CONFIG_NAME, SCALA_INCREMENTAL_ANALYSIS_CONFIGURATION_PREFIX, KTLINT_CONFIGURATION_PREFIX, REPOSITORY_CONTENT_DESCRIPTOR_CONFIGURATION_PREFIX)
4547
private val ignoredConfigurationSuffixes = listOf(PMD_CONFIGURATION_SUFFIX)
4648

47-
companion object Constants {
49+
companion object {
50+
val Logger: Logger = Logging.getLogger(ResolutionRulesPlugin::class.java)
51+
52+
const val NEBULA_RECOMMENDER_BOM_CONFIG_NAME: String = "nebulaRecommenderBom"
4853
const val SPRING_VERSION_MANAGEMENT_CONFIG_NAME = "versionManagement"
4954
const val KTLINT_CONFIGURATION_PREFIX = "ktlint"
5055
const val PMD_CONFIGURATION_SUFFIX = "PmdAuxClasspath"
@@ -59,18 +64,32 @@ class ResolutionRulesPlugin : Plugin<Project> {
5964
override fun apply(project: Project) {
6065
this.project = project
6166
configurations = project.configurations
62-
extension = project.extensions.create("nebulaResolutionRules", NebulaResolutionRulesExtension::class.java, project)
67+
extension =
68+
project.extensions.create("nebulaResolutionRules", NebulaResolutionRulesExtension::class.java, project)
6369

6470
project.onExecute {
6571
if (isCoreAlignmentEnabled()) {
66-
logger.info("${project.name}: coreAlignmentSupport feature enabled")
72+
Logger.info("${project.name}: coreAlignmentSupport feature enabled")
6773
}
6874
}
6975

7076
val rootProject = project.rootProject
71-
rootProject.configurations.maybeCreate(RESOLUTION_RULES_CONFIG_NAME)
77+
val configuration = project.configurations.maybeCreate(RESOLUTION_RULES_CONFIG_NAME)
78+
if (project != rootProject) {
79+
configuration.isCanBeConsumed = false
80+
val rootProjectDependency = project.dependencies.project(
81+
mapOf("path" to rootProject.path, "configuration" to RESOLUTION_RULES_CONFIG_NAME)
82+
)
83+
configuration.withDependencies { dependencies ->
84+
dependencies.add(rootProjectDependency)
85+
}
86+
}
7287
if (rootProject.extensions.findByType(NebulaResolutionRulesExtension::class.java) == null) {
73-
rootProject.extensions.create("nebulaResolutionRules", NebulaResolutionRulesExtension::class.java, rootProject)
88+
rootProject.extensions.create(
89+
"nebulaResolutionRules",
90+
NebulaResolutionRulesExtension::class.java,
91+
rootProject
92+
)
7493
}
7594

7695
project.configurations.all { config ->
@@ -86,7 +105,9 @@ class ResolutionRulesPlugin : Plugin<Project> {
86105
project.onExecute {
87106
val ruleSet = extension.ruleSet()
88107
when {
89-
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")
108+
config.state != Configuration.State.UNRESOLVED || config.getObservedState() != Configuration.State.UNRESOLVED -> Logger.warn(
109+
"Dependency resolution rules will not be applied to $config, it was resolved before the project was executed"
110+
)
90111
else -> {
91112
ruleSet.dependencyRulesPartOne().forEach { rule ->
92113
rule.apply(project, config, config.resolutionStrategy, extension)
@@ -102,7 +123,7 @@ class ResolutionRulesPlugin : Plugin<Project> {
102123

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

126-
open class NebulaResolutionRulesExtension @Inject constructor(private val project: Project) {
147+
@Suppress("UnstableApiUsage")
148+
abstract class NebulaResolutionRulesService : BuildService<NebulaResolutionRulesService.Params> {
127149
companion object {
128-
private val logger: Logger = Logging.getLogger(ResolutionRulesPlugin::class.java)
129-
private val mapper = objectMapper()
130-
}
131-
132-
var include = ArrayList<String>()
133-
var optional = ArrayList<String>()
134-
var exclude = ArrayList<String>()
135-
var useCoreGradleAlignment = false
150+
private val Logger: Logger = Logging.getLogger(NebulaResolutionRulesService::class.java)
151+
private val Mapper = objectMapper()
152+
153+
fun registerService(project: Project): Provider<NebulaResolutionRulesService> {
154+
return project.gradle.sharedServices.registerIfAbsent(
155+
"nebulaResolutionRules",
156+
NebulaResolutionRulesService::class.java
157+
) { spec ->
158+
val resolutionRules = resolveResolutionRules(project)
159+
spec.parameters.getResolutionRules().set(ResolutionRules(resolutionRules))
160+
}
161+
}
136162

137-
private val rulesByFile by lazy {
138-
check(project == project.rootProject) { "This should only be called on the root project extension" }
139-
val configuration = project.configurations.getByName(RESOLUTION_RULES_CONFIG_NAME)
140-
val files = project.copyConfiguration(configuration).resolve()
141-
val rules = LinkedHashMap<String, RuleSet>()
142-
for (file in files) {
143-
val filename = file.name
144-
logger.debug("nebula.resolution-rules uses: $filename")
145-
if (filename.endsWith(ResolutionRulesPlugin.JSON_EXT)) {
146-
rules.putRules(mapper.parseJsonFile(file))
147-
} else if (filename.endsWith(ResolutionRulesPlugin.JAR_EXT) || filename.endsWith(ResolutionRulesPlugin.ZIP_EXT)) {
148-
logger.info("nebula.resolution-rules is using ruleset: $filename")
149-
ZipFile(file).use { zip ->
150-
val entries = zip.entries()
151-
while (entries.hasMoreElements()) {
152-
val entry = entries.nextElement()
153-
if (entry.name.endsWith(ResolutionRulesPlugin.JSON_EXT)) {
154-
rules.putRules(mapper.parseJsonStream(zip, entry))
163+
private fun resolveResolutionRules(project: Project): Map<String, RuleSet> {
164+
val configuration = project.configurations.getByName(RESOLUTION_RULES_CONFIG_NAME)
165+
val files = configuration.resolve()
166+
val rules = LinkedHashMap<String, RuleSet>()
167+
for (file in files) {
168+
val filename = file.name
169+
Logger.debug("nebula.resolution-rules uses: $filename")
170+
if (filename.endsWith(ResolutionRulesPlugin.JSON_EXT)) {
171+
rules.putRules(Mapper.parseJsonFile(file))
172+
} else if (filename.endsWith(ResolutionRulesPlugin.JAR_EXT) || filename.endsWith(ResolutionRulesPlugin.ZIP_EXT)) {
173+
Logger.info("nebula.resolution-rules is using ruleset: $filename")
174+
ZipFile(file).use { zip ->
175+
val entries = zip.entries()
176+
while (entries.hasMoreElements()) {
177+
val entry = entries.nextElement()
178+
if (entry.name.endsWith(ResolutionRulesPlugin.JSON_EXT)) {
179+
rules.putRules(Mapper.parseJsonStream(zip, entry))
180+
}
155181
}
156182
}
183+
} else {
184+
Logger.debug("Unsupported rules file extension for $file")
157185
}
158-
} else {
159-
logger.debug("Unsupported rules file extension for $file")
186+
}
187+
return rules
188+
}
189+
190+
private fun MutableMap<String, RuleSet>.putRules(ruleSet: RuleSet) {
191+
if (put(ruleSet.name!!, ruleSet) != null) {
192+
Logger.info("Found rules with the same name. Overriding existing ruleset ${ruleSet.name}")
160193
}
161194
}
162-
rules
195+
196+
private fun ruleSetName(filename: String) =
197+
filename.substring(0, filename.lastIndexOf(ResolutionRulesPlugin.JSON_EXT))
198+
199+
private fun ObjectMapper.parseJsonFile(file: File): RuleSet {
200+
val ruleSetName = ruleSetName(file.name)
201+
Logger.debug("Using $ruleSetName (${file.name}) a dependency rules source")
202+
return readValue<RuleSet>(file).withName(ruleSetName)
203+
}
204+
205+
private fun ObjectMapper.parseJsonStream(zip: ZipFile, entry: ZipEntry): RuleSet {
206+
val ruleSetName = ruleSetName(File(entry.name).name)
207+
Logger.debug("Using $ruleSetName (${zip.name}) a dependency rules source")
208+
return readValue<RuleSet>(zip.getInputStream(entry)).withName(ruleSetName)
209+
}
210+
}
211+
212+
interface Params : BuildServiceParameters {
213+
fun getResolutionRules(): Property<ResolutionRules>
163214
}
164215

216+
class ResolutionRules(val byFile: Map<String, RuleSet>) : Serializable
217+
}
218+
219+
open class NebulaResolutionRulesExtension @Inject constructor(private val project: Project) {
220+
var include = ArrayList<String>()
221+
var optional = ArrayList<String>()
222+
var exclude = ArrayList<String>()
223+
var useCoreGradleAlignment = false
224+
165225
fun ruleSet(): RuleSet {
166-
val extension = project.rootProject.extensions.getByType(NebulaResolutionRulesExtension::class.java)
167-
return extension.rulesByFile.filterKeys { ruleSet ->
226+
val service = NebulaResolutionRulesService.registerService(project).get()
227+
@Suppress("UnstableApiUsage") val rulesByFile = service.parameters
228+
.getResolutionRules()
229+
.get()
230+
.byFile
231+
return rulesByFile.filterKeys { ruleSet ->
168232
when {
169233
ruleSet.startsWith(ResolutionRulesPlugin.OPTIONAL_PREFIX) -> {
170234
val ruleSetWithoutPrefix = ruleSet.substring(ResolutionRulesPlugin.OPTIONAL_PREFIX.length)
@@ -175,24 +239,4 @@ open class NebulaResolutionRulesExtension @Inject constructor(private val projec
175239
}
176240
}.values.flatten()
177241
}
178-
179-
private fun MutableMap<String, RuleSet>.putRules(ruleSet: RuleSet) {
180-
if (put(ruleSet.name!!, ruleSet) != null) {
181-
logger.info("Found rules with the same name. Overriding existing ruleset ${ruleSet.name}")
182-
}
183-
}
184-
185-
private fun ruleSetName(filename: String) = filename.substring(0, filename.lastIndexOf(ResolutionRulesPlugin.JSON_EXT))
186-
187-
private fun ObjectMapper.parseJsonFile(file: File): RuleSet {
188-
val ruleSetName = ruleSetName(file.name)
189-
logger.debug("Using $ruleSetName (${file.name}) a dependency rules source")
190-
return readValue<RuleSet>(file).withName(ruleSetName)
191-
}
192-
193-
private fun ObjectMapper.parseJsonStream(zip: ZipFile, entry: ZipEntry): RuleSet {
194-
val ruleSetName = ruleSetName(File(entry.name).name)
195-
logger.debug("Using $ruleSetName (${zip.name}) a dependency rules source")
196-
return readValue<RuleSet>(zip.getInputStream(entry)).withName(ruleSetName)
197-
}
198242
}

src/main/kotlin/nebula/plugin/resolutionrules/rules.kt

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,12 @@ import org.gradle.api.artifacts.*
2323
import org.gradle.api.artifacts.component.ModuleComponentSelector
2424
import org.gradle.api.internal.artifacts.DefaultModuleIdentifier
2525
import org.gradle.api.internal.artifacts.DefaultModuleVersionIdentifier
26-
import org.gradle.api.internal.artifacts.dsl.ModuleVersionSelectorParsers
2726
import org.gradle.api.internal.artifacts.ivyservice.dependencysubstitution.DefaultDependencySubstitutions
2827
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.ExactVersionSelector
2928
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionSelector
30-
import org.gradle.api.logging.Logger
31-
import org.gradle.api.logging.Logging
3229
import java.io.Serializable
3330

34-
interface Rule {
31+
interface Rule : Serializable {
3532
fun apply(
3633
project: Project,
3734
configuration: Configuration,
@@ -59,7 +56,7 @@ data class RuleSet(
5956
val deny: List<DenyRule> = emptyList(),
6057
val exclude: List<ExcludeRule> = emptyList(),
6158
val align: List<AlignRule> = emptyList()
62-
) {
59+
) : Serializable {
6360

6461
fun dependencyRulesPartOne() =
6562
listOf(replace, deny, exclude).flatten() + listOf(SubstituteRules(substitute), RejectRules(reject))
@@ -131,13 +128,9 @@ data class SubstituteRule(
131128
val module: String, val with: String, override var ruleSet: String?,
132129
override val reason: String, override val author: String, override val date: String
133130
) : BasicRule, Serializable {
134-
lateinit var substitutedVersionId: ModuleVersionIdentifier
135-
lateinit var withComponentSelector: ModuleComponentSelector
136-
private val versionSelector by lazy {
137-
check(substitutedVersionId.version.isNotEmpty()) { "Version may not be empty" }
138-
val version = substitutedVersionId.version
139-
VersionWithSelector(version).asSelector()
140-
}
131+
@Transient lateinit var substitutedVersionId: ModuleVersionIdentifier
132+
@Transient lateinit var withComponentSelector: ModuleComponentSelector
133+
@Transient lateinit var versionSelector: VersionSelector
141134

142135
override fun apply(
143136
project: Project,
@@ -169,7 +162,7 @@ class SubstituteRules(val rules: List<SubstituteRule>) : Rule {
169162
).apply { isAccessible = true }
170163
}
171164

172-
private lateinit var rulesById: Map<ModuleIdentifier, List<SubstituteRule>>
165+
@Transient private lateinit var rulesById: Map<ModuleIdentifier, List<SubstituteRule>>
173166

174167
override fun apply(
175168
project: Project,
@@ -187,6 +180,7 @@ class SubstituteRules(val rules: List<SubstituteRule>) : Rule {
187180
throw SubstituteRuleMissingVersionException(rule.with, rule)
188181
}
189182
rule.withComponentSelector = withModule
183+
rule.versionSelector = VersionWithSelector(rule.substitutedVersionId.version).asSelector()
190184
}
191185
rule
192186
}.groupBy { it.substitutedVersionId.module }
@@ -232,7 +226,7 @@ data class RejectRule(
232226
override val date: String
233227
) : ModuleRule {
234228
val moduleVersionId = module.toModuleVersionId()
235-
lateinit var versionSelector: VersionSelector
229+
@Transient lateinit var versionSelector: VersionSelector
236230

237231
init {
238232
if (moduleVersionId.version.isNotEmpty()) {
@@ -313,7 +307,6 @@ data class ExcludeRule(
313307
override val author: String,
314308
override val date: String
315309
) : ModuleRule {
316-
private val logger: Logger = Logging.getLogger(ExcludeRule::class.java)
317310
private val moduleId = module.toModuleId()
318311

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

0 commit comments

Comments
 (0)