Skip to content

Commit e2672b0

Browse files
committed
Register a single module-only substitution rule
This avoids inherent performance issues in both dependencySubstitutions.all and resolutionStrategy.eachDependency and improves performance of both unversioned and versioned substitutions.
1 parent d110119 commit e2672b0

File tree

1 file changed

+53
-54
lines changed
  • src/main/kotlin/nebula/plugin/resolutionrules

1 file changed

+53
-54
lines changed

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

Lines changed: 53 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
package nebula.plugin.resolutionrules
1818

1919
import com.netflix.nebula.interop.VersionWithSelector
20+
import org.gradle.api.Action
2021
import org.gradle.api.Project
2122
import org.gradle.api.artifacts.*
22-
import org.gradle.api.artifacts.component.ComponentSelector
2323
import org.gradle.api.artifacts.component.ModuleComponentSelector
2424
import org.gradle.api.internal.artifacts.DefaultModuleIdentifier
2525
import org.gradle.api.internal.artifacts.DefaultModuleVersionIdentifier
2626
import org.gradle.api.internal.artifacts.dsl.ModuleVersionSelectorParsers
27+
import org.gradle.api.internal.artifacts.ivyservice.dependencysubstitution.DefaultDependencySubstitutions
28+
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.ExactVersionSelector
2729
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionSelector
2830
import org.gradle.api.logging.Logger
2931
import org.gradle.api.logging.Logging
@@ -129,11 +131,11 @@ data class SubstituteRule(
129131
val module: String, val with: String, override var ruleSet: String?,
130132
override val reason: String, override val author: String, override val date: String
131133
) : BasicRule, Serializable {
132-
lateinit var substitutedModule: ComponentSelector
134+
lateinit var substitutedVersionId: ModuleVersionIdentifier
133135
lateinit var withComponentSelector: ModuleComponentSelector
134-
lateinit var withVersionSelector: ModuleVersionSelector
135-
val versionSelector by lazy {
136-
val version = (substitutedModule as ModuleComponentSelector).version
136+
private val versionSelector by lazy {
137+
check(substitutedVersionId.version.isNotEmpty()) { "Version may not be empty" }
138+
val version = substitutedVersionId.version
137139
VersionWithSelector(version).asSelector()
138140
}
139141

@@ -146,82 +148,79 @@ data class SubstituteRule(
146148
throw UnsupportedOperationException("Substitution rules cannot be applied directly and must be applied via SubstituteRules")
147149
}
148150

149-
fun isInitialized(): Boolean = this::substitutedModule.isInitialized
151+
fun isInitialized(): Boolean = this::substitutedVersionId.isInitialized
152+
153+
fun acceptsVersion(version: String): Boolean {
154+
return if (substitutedVersionId.version.isNotEmpty()) {
155+
when (VersionWithSelector(version).asSelector()) {
156+
is ExactVersionSelector -> versionSelector.accept(version)
157+
else -> false
158+
}
159+
} else true
160+
}
150161
}
151162

152163
class SubstituteRules(val rules: List<SubstituteRule>) : Rule {
153-
private lateinit var versionedRulesById: Map<ModuleIdentifier, List<SubstituteRule>>
154-
private lateinit var unversionedRules: List<SubstituteRule>
164+
companion object {
165+
private val SUBSTITUTIONS_ADD_RULE = DefaultDependencySubstitutions::class.java.getDeclaredMethod(
166+
"addSubstitution",
167+
Action::class.java,
168+
Boolean::class.java
169+
).apply { isAccessible = true }
170+
}
171+
172+
private lateinit var rulesById: Map<ModuleIdentifier, List<SubstituteRule>>
155173

156174
override fun apply(
157175
project: Project,
158176
configuration: Configuration,
159177
resolutionStrategy: ResolutionStrategy,
160178
extension: NebulaResolutionRulesExtension
161179
) {
162-
if (!this::versionedRulesById.isInitialized) {
180+
if (!this::rulesById.isInitialized) {
163181
val substitution = resolutionStrategy.dependencySubstitution
164-
165-
val (versionedRules, unversionedRules) = rules.map { rule ->
182+
rulesById = rules.map { rule ->
166183
if (!rule.isInitialized()) {
167-
rule.substitutedModule = substitution.module(rule.module)
184+
rule.substitutedVersionId = rule.module.toModuleVersionId()
168185
val withModule = substitution.module(rule.with)
169186
if (withModule !is ModuleComponentSelector) {
170187
throw SubstituteRuleMissingVersionException(rule.with, rule)
171188
}
172189
rule.withComponentSelector = withModule
173-
rule.withVersionSelector = ModuleVersionSelectorParsers.parser()
174-
.parseNotation(rule.withComponentSelector.displayName)
175190
}
176191
rule
177-
}.partition { it.substitutedModule is ModuleComponentSelector }
178-
179-
this.unversionedRules = unversionedRules
180-
versionedRulesById =
181-
versionedRules.groupBy { (it.substitutedModule as ModuleComponentSelector).moduleIdentifier }
192+
}.groupBy { it.substitutedVersionId.module }
193+
.mapValues { entry -> entry.value.sortedBy { it.substitutedVersionId.version } }
182194
}
183195

184-
if (versionedRulesById.isNotEmpty()) {
185-
// We use eachDependency because dependencySubstitutions.all causes configuration task dependencies to resolve at configuration time
186-
resolutionStrategy.eachDependency { details ->
187-
val requested = details.requested
188-
val requestedString = requested.toString()
189-
val rules = versionedRulesById[requested.module] ?: return@eachDependency
190-
val requestedSelectorVersion = requested.version
196+
val substitutionAction = Action<DependencySubstitution> { details ->
197+
val requested = details.requested
198+
if (requested is ModuleComponentSelector) {
199+
val rules = rulesById[requested.moduleIdentifier] ?: return@Action
191200
rules.forEach { rule ->
192-
if (rule.versionSelector.accept(requestedSelectorVersion)
193-
&& !requestedString.contains(".+")
194-
&& !requestedString.contains("latest")
195-
) {
196-
// Note on `useTarget`:
197-
// Forcing modules via ResolutionStrategy.force(Object...) uses this capability.
198-
// from https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/DependencyResolveDetails.html
199-
details.useTarget(rule.withVersionSelector) // We can't pass a ModuleComponentSelector here so we take the conversion hit
200-
details.because("substituted ${rule.substitutedModule} with ${rule.withComponentSelector} because '${rule.reason}' by rule ${rule.ruleSet}")
201-
return@eachDependency
201+
val withComponentSelector = rule.withComponentSelector
202+
if (rule.acceptsVersion(requested.version)) {
203+
val message =
204+
"substituted ${rule.substitutedVersionId} with $withComponentSelector because '${rule.reason}' by rule ${rule.ruleSet}"
205+
details.useTarget(
206+
withComponentSelector,
207+
message
208+
)
209+
return@Action
202210
}
203211
}
204212
}
205213
}
206214

207-
unversionedRules.forEach { rule ->
208-
val substitutedModule = rule.substitutedModule
209-
val withComponentSelector = rule.withComponentSelector
210-
var message = "substituted $withComponentSelector because '${rule.reason}' by rule ${rule.ruleSet}"
211-
212-
val selectorNameSections = substitutedModule.displayName.split(":")
213-
if (selectorNameSections.size > 2) {
214-
val selectorGroupAndArtifact = "${selectorNameSections[0]}:${selectorNameSections[1]}"
215-
message =
216-
"substituted $selectorGroupAndArtifact with $withComponentSelector because '${rule.reason}' by rule ${rule.ruleSet}"
217-
}
218-
219-
resolutionStrategy.dependencySubstitution {
220-
it.substitute(substitutedModule)
221-
.because(message)
222-
.with(withComponentSelector)
223-
}
224-
}
215+
/*
216+
* Unfortunately impossible to avoid an internal/protected method dependency for now:
217+
*
218+
* - We can't dependencySubstitutions.all because it causes the configuration to be resolved at task graph calculation time due to the possibility of project substitutions there
219+
* - Likewise eachDependency has it's own performance issues - https://github.com/gradle/gradle/issues/16151
220+
*
221+
* There's no alternative to all that only allows module substitution and we only ever substitute modules for modules, so this is completely safe.
222+
*/
223+
SUBSTITUTIONS_ADD_RULE.invoke(resolutionStrategy.dependencySubstitution, substitutionAction, false)
225224
}
226225
}
227226

0 commit comments

Comments
 (0)