All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nebula.plugin.resolutionrules.alignRule.kt Maven / Gradle / Ivy

There is a newer version: 11.4.1
Show newest version
package nebula.plugin.resolutionrules

import com.netflix.nebula.interop.VersionWithSelector
import com.netflix.nebula.interop.selectedId
import com.netflix.nebula.interop.selectedModuleVersion
import com.netflix.nebula.interop.selectedVersion
import org.gradle.api.Action
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.artifacts.*
import org.gradle.api.artifacts.ModuleVersionIdentifier
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ModuleComponentSelector
import org.gradle.api.artifacts.result.ComponentSelectionCause
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.artifacts.result.UnresolvedDependencyResult
import org.gradle.api.internal.ReusableAction
import org.gradle.api.internal.artifacts.DefaultModuleIdentifier
import org.gradle.api.internal.artifacts.DefaultModuleVersionIdentifier
import org.gradle.api.internal.artifacts.DefaultModuleVersionSelector
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.LatestVersionSelector
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.SubVersionSelector
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionRangeSelector
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import java.io.Serializable
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Matcher
import java.util.regex.Pattern
import javax.inject.Inject

data class AlignRule(val name: String?,
                     val group: Regex,
                     val includes: List = emptyList(),
                     val excludes: List = emptyList(),
                     val match: String?,
                     override var ruleSet: String?,
                     override val reason: String,
                     override val author: String,
                     override val date: String,
                     var belongsToName: String?) : BasicRule, Serializable {

    private val groupPattern = group.toPattern()
    private val includesPatterns = includes.map { it.toPattern() }
    private val excludesPatterns = excludes.map { it.toPattern() }
    private val alignMatchers = ConcurrentHashMap()

    override fun apply(project: Project,
                       configuration: Configuration,
                       resolutionStrategy: ResolutionStrategy,
                       extension: NebulaResolutionRulesExtension) {
        //TODO this rule is applied repeatedly for each configuration. Ideally it should be taken out and
        //applied only once per project
        if (configuration.name == "compileClasspath") { // This is one way to ensure it'll be run for only one configuration
            project.dependencies.components.all(AlignedPlatformMetadataRule::class.java) {
                it.params(this)
            }
        }
    }

    fun ruleMatches(dep: ModuleVersionIdentifier) = ruleMatches(dep.group, dep.name)

    fun ruleMatches(group: String, name: String) = alignMatchers.computeIfAbsent(Thread.currentThread()) {
        AlignMatcher(this, groupPattern, includesPatterns, excludesPatterns)
    }.matches(group, name)
}

class AlignMatcher(val rule: AlignRule, groupPattern: Pattern, includesPatterns: List, excludesPatterns: List) {
    private val groupMatcher = groupPattern.matcher("")
    private val includeMatchers = includesPatterns.map { it.matcher("") }
    private val excludeMatchers = excludesPatterns.map { it.matcher("") }

    private fun Matcher.matches(input: String, type: String): Boolean {
        reset(input)
        return try {
            matches()
        } catch (e: Exception) {
            throw java.lang.IllegalArgumentException("Failed to use matcher '$this' from type '$type' to match '$input'\n" +
                    "Rule: $rule", e)
        }
    }

    fun matches(group: String, name: String): Boolean {
        return groupMatcher.matches(group, "group") &&
                (includeMatchers.isEmpty() || includeMatchers.any { it.matches(name, "includes") }) &&
                (excludeMatchers.isEmpty() || excludeMatchers.none { it.matches(name, "excludes") })
    }
}

@CacheableRule
open class AlignedPlatformMetadataRule @Inject constructor(val rule: AlignRule) : ComponentMetadataRule, Serializable, ReusableAction {
    private val logger: Logger = Logging.getLogger(AlignedPlatformMetadataRule::class.java)

    override fun execute(componentMetadataContext: ComponentMetadataContext?) {
        modifyDetails(componentMetadataContext!!.details)
    }

    fun modifyDetails(details: ComponentMetadataDetails) {
        if (rule.ruleMatches(details.id)) {
            details.belongsTo("aligned-platform:${rule.belongsToName}:${details.id.version}")
            logger.debug("Aligning platform based on '${details.id.group}:${details.id.name}:${details.id.version}' from align rule with group '${rule.group}'")
        }
    }
}

data class AlignRules(val aligns: List) : Rule {
    companion object {
        val logger: Logger = Logging.getLogger(AlignRules::class.java)

        const val MAX_PASSES = 5
    }

    override fun apply(project: Project, configuration: Configuration, resolutionStrategy: ResolutionStrategy, extension: NebulaResolutionRulesExtension) {
        if (configuration.isCopy) {
            // Don't attempt to align one of our copied configurations
            return
        }

        if (aligns.isEmpty()) {
            logger.debug("Skipping alignment for $configuration - No alignment rules are configured")
            return
        }

        if (!configuration.isTransitive) {
            logger.debug("Skipping alignment for $configuration - Configuration is not transitive")
            return
        }

        val baselineAligns = project
                .copyConfiguration(configuration)
                .baselineAligns()

        if (baselineAligns.isEmpty()) {
            logger.debug("Short-circuiting alignment for $configuration - No align rules matched the configured configurations")
            return
        }

        val stableAligns = project
                .copyConfiguration(configuration)
                .applyAligns(baselineAligns)
                .stableResolvedAligns(baselineAligns)

        configuration.applyAligns(stableAligns, true)
    }

    private fun CopiedConfiguration.baselineAligns(): List =
            selectedVersions({ it.selectedModuleVersion })
                    .filter {
                        val resolvedVersions = it.resolvedDependencies
                                .mapToSet { it.selectedVersion }
                        resolvedVersions.size > 1 || resolvedVersions.single() != it.alignedVersion.version.stringVersion
                    }

    private fun CopiedConfiguration.resolvedAligns(baselineAligns: List) =
            selectedVersions({ dependency ->
                val selectedModuleVersion = dependency.selectedModuleVersion
                val alignedVersion = baselineAligns.singleOrNull {
                    it.ruleMatches(selectedModuleVersion)
                }
                if (alignedVersion.useRequestedVersion(dependency)) {
                    /**
                     * If the selected version for an aligned dependency was unaffected by resolutionStrategies etc.,
                     * then we choose the requested version so we can reflect the requested dependency pre-alignment.
                     *
                     * We ignore dynamic selectors, which would be a problem when an aligned dependency brings in a
                     * dynamic selector - but we'll have to live with that very small chance of inconsistency while
                     * we have to do alignment like this...
                     */
                    // FIXME the requested version is the last seen, not the one that contributed the highest version
                    val selector = dependency.requested
                    if (selector is ModuleComponentSelector) {
                        if (VersionWithSelector(selector.version).asSelector().isDynamic) {
                            selectedModuleVersion
                        } else {
                            DefaultModuleVersionIdentifier.newId(selector.group, selector.module, selector.version)
                        }
                    } else {
                        selectedModuleVersion
                    }
                } else {
                    selectedModuleVersion
                }
            })

    private tailrec fun CopiedConfiguration.stableResolvedAligns(baselineAligns: List, pass: Int = 1): List {
        check(pass <= MAX_PASSES) {
            "The maximum number of alignment passes were attempted ($MAX_PASSES) for $source"
        }
        val resolvedAligns = resolvedAligns(baselineAligns)
        val copy = copyConfiguration().applyAligns(resolvedAligns)
        val copyResolvedAligns = copy.resolvedAligns(baselineAligns)
        return when {
            resolvedAligns.isEmpty() -> copyResolvedAligns // Alignment caused the configuration to be unresolvable, apply the broken alignments so that failures bubble up
            resolvedAligns != copyResolvedAligns -> copy.stableResolvedAligns(baselineAligns, pass.inc())
            else -> resolvedAligns
        }
    }

    private fun AlignedVersionWithDependencies?.useRequestedVersion(newRoundDependency: ResolvedDependencyResult): Boolean {
        if (this == null) {
            return false
        }
        //resolved dependencies contain the same dependency multiple times based on who brought it when we compare it
        //with a newly resolved dependency from the next alignment run we need to much the same from
        val dependency = resolvedDependencies
                .filter { it.from.moduleVersion?.module == newRoundDependency.from.moduleVersion?.module }
                .map { it.selected }
                .distinct()
                .singleOrNull { it.moduleVersion?.module == newRoundDependency.selectedModuleVersion.module }
                ?: return true
        val selectionReason = dependency.selectionReason
        return selectionReason
                .descriptions
                .map { it.cause }
                .all { it == ComponentSelectionCause.REQUESTED || it == ComponentSelectionCause.CONFLICT_RESOLUTION }
    }

    private fun AlignedVersionWithDependencies.ruleMatches(dep: ModuleVersionIdentifier) = alignedVersion.rule.ruleMatches(dep)

    @Suppress("UNCHECKED_CAST")
    private fun CopiedConfiguration.selectedVersions(versionSelector: (ResolvedDependencyResult) -> ModuleVersionIdentifier): List {
        val partitioned = incoming.resolutionResult.allDependencies
                .partition { it is ResolvedDependencyResult }
        val resolved = partitioned.first as List
        val unresolved = partitioned.second as List

        if (unresolved.isNotEmpty()) {
            val unresolvedDetails = unresolved.distinct().joinToString("\n") { " - $it" }
            val message = "Resolution rules could not resolve all dependencies to align $source:\n$unresolvedDetails"
            logger.error(message)
            return emptyList()
        }

        val resolvedDependencies = resolved
                .filter { it.selectedId is ModuleComponentIdentifier }
        val resolvedVersions = resolvedDependencies
                .map(versionSelector)
                .distinct()

        val selectedVersions = ArrayList()
        aligns.forEach { align ->
            val matches = resolvedVersions.filter { dep: ModuleVersionIdentifier -> align.ruleMatches(dep) }
            if (matches.isNotEmpty()) {
                val version = alignedRange(align, matches, this)
                selectedVersions += AlignedVersion(align, version).addResolvedDependencies(resolvedDependencies)
            }
        }
        return selectedVersions
    }

    private data class AlignedVersion(val rule: AlignRule, val version: VersionWithSelector)

    private data class AlignedVersionWithDependencies(val alignedVersion: AlignedVersion) {
        // Non-constructor property to prevent it from being included in equals/hashcode
        lateinit var resolvedDependencies: List
    }

    private fun AlignedVersion.addResolvedDependencies(resolvedDependencies: List): AlignedVersionWithDependencies {
        val withDependencies = AlignedVersionWithDependencies(this)
        withDependencies.resolvedDependencies = resolvedDependencies.filter {
            val moduleVersion = it.selectedModuleVersion
            rule.ruleMatches(moduleVersion)
        }
        return withDependencies
    }

    private fun CopiedConfiguration.applyAligns(alignedVersionsWithDependencies: List) = (this as Configuration).applyAligns(alignedVersionsWithDependencies) as CopiedConfiguration

    private fun Configuration.applyAligns(alignedVersionsWithDependencies: List, finalConfiguration: Boolean = false): Configuration {
        alignedVersionsWithDependencies.map { it.alignedVersion }.let {
            resolutionStrategy.eachDependency(ApplyAlignsAction(it, finalConfiguration))
        }
        return this
    }

    private inner class ApplyAlignsAction(val alignedVersions: List, val finalConfiguration: Boolean) : Action {
        override fun execute(details: DependencyResolveDetails) {
            val target = details.target
            val alignedVersion = alignedVersions.firstOrNull {
                it.rule.ruleMatches(target.group, target.name)
            }
            if (alignedVersion != null) {
                val (rule, version) = alignedVersion
                if (version.stringVersion != details.target.version) {
                    if (finalConfiguration) {
                        logger.debug("Resolution rule $rule aligning ${details.requested.group}:${details.requested.name} to $version")
                    }
                    details.because("aligned to $version by rule ${rule.ruleSet} aligning group '${rule.group}'")
                            .useVersion("(,$version]")
                }
            }
        }
    }

    private fun alignedRange(rule: AlignRule, moduleVersions: List, configuration: Configuration): VersionWithSelector {
        try {
            val versions = moduleVersions.mapToSet { VersionWithSelector(it.version) }
            check(versions.all { !it.asSelector().isDynamic }) { "A dynamic version was included in $versions for $rule" }
            val highestVersion = versions.max()!!

            val forcedModules = moduleVersions.flatMap { moduleVersion ->
                configuration.resolutionStrategy.forcedModules.filter {
                    it.group == moduleVersion.group && it.name == moduleVersion.name
                }
            }

            val forcedDependencies = moduleVersions.flatMap { moduleVersion ->
                configuration.dependencies.filter {
                    it is ExternalDependency && it.isForce && it.group == moduleVersion.group && it.name == moduleVersion.name
                }
            }.map {
                val moduleIdentifier = DefaultModuleIdentifier.newId(it.group, it.name)
                @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
                DefaultModuleVersionSelector.newSelector(moduleIdentifier, it.version)
            }

            val forced = forcedModules + forcedDependencies
            if (forced.isNotEmpty()) {
                val (dynamic, static) = forced
                        .mapToSet { VersionWithSelector(it.version!!) }
                        .partition { it.asSelector().isDynamic }
                return if (static.isNotEmpty()) {
                    val forcedVersion = static.min()!!
                    logger.debug("Found force(s) $forced that supersede resolution rule $rule. Will use $forcedVersion") // FIXME: What about locks?
                    forcedVersion
                } else {
                    val mostSpecific = dynamic.minBy {
                        when (it.asSelector().javaClass.kotlin) {
                            LatestVersionSelector::class -> 2
                            SubVersionSelector::class -> 1
                            VersionRangeSelector::class -> 0
                            else -> throw IllegalArgumentException("Unknown selector type $it")
                        }
                    }!!
                    val forcedVersion = if (mostSpecific.asSelector() is LatestVersionSelector) {
                        highestVersion
                    } else {
                        versions.filter { mostSpecific.asSelector().accept(it.stringVersion) }.max()!!
                    }
                    logger.debug("Found force(s) $forced that supersede resolution rule $rule. Will use highest dynamic version $forcedVersion that matches most specific selector $mostSpecific") // FIXME: What about locks?
                    forcedVersion
                }
            }

            return highestVersion
        } catch (e: Exception) {
            throw GradleException("Could not apply alignment rule ${rule.name} | Reason: ${e.message}", e)
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy