Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package datadog.gradle.plugin.muzzle

import groovy.lang.Closure
import org.eclipse.aether.repository.RemoteRepository
import org.eclipse.aether.util.version.GenericVersionScheme
import org.eclipse.aether.version.InvalidVersionSpecificationException
import org.gradle.api.Action
import java.io.Serializable

/**
Expand All @@ -23,6 +27,7 @@ open class MuzzleDirective : Serializable {
var additionalDependencies: MutableList<String> = ArrayList()
internal var additionalRepositories: MutableList<Triple<String, String, String>> = ArrayList()
internal var excludedDependencies: MutableList<String> = ArrayList()
internal var mavenPomOverrideConfig: MavenPomOverrides? = null
var assertPass: Boolean = false
var assertInverse: Boolean = false
var skipFromReport: Boolean = false
Expand Down Expand Up @@ -64,6 +69,27 @@ open class MuzzleDirective : Serializable {
excludedDependencies.add(excludeString)
}

/**
* Rewrites dependency versions declared in the published POMs of this directive's artifacts.
*
* Use this when a dependency muzzle does not care about makes a POM unresolvable, for instance when
* upstream references a version it never published. Gradle resolution rules cannot help there: a bad
* version in a parent POM or an imported BOM breaks POM parsing before any rule runs, so the POM is
* patched after download and before Maven builds the model. Artifacts matched by
* [MavenPomOverrides.artifactVersions] are consequently resolved by Maven instead of Gradle.
* Overrides apply only to this directive, not to inverse directives generated from it.
*/
fun mavenPomOverrides(action: Action<in MavenPomOverrides>) {
mavenPomOverrideConfig = MavenPomOverrides().also(action::execute).also(MavenPomOverrides::validate)
}

internal fun requiresMavenResolution(version: String): Boolean {
val overrides = mavenPomOverrideConfig ?: return false
val versionScheme = GenericVersionScheme()
return versionScheme.parseVersionConstraint(overrides.artifactVersions)
.containsVersion(versionScheme.parseVersion(version))
}

/**
* Get the list of repositories to use for this muzzle directive.
*
Expand Down Expand Up @@ -97,3 +123,128 @@ open class MuzzleDirective : Serializable {
"${if (assertPass) "pass" else "fail"} $group:$module:$versions"
}
}

/** Overrides dependency versions embedded in published Maven POMs for selected artifact versions. */
open class MavenPomOverrides : Serializable {
/**
* Maven version range selecting which of this directive's artifact versions get their POMs rewritten,
* for example `"[7.4,8)"`. Versions outside the range are resolved by Gradle as usual. Required, so
* that rewriting POMs of versions that do not need it is always a deliberate choice; use `"[,)"` to
* opt every version of the directive in.
*/
var artifactVersions: String = ""
internal val dependencyVersionOverrides: MutableMap<String, MutableMap<String, String>> =
LinkedHashMap()
internal val dependencyVersionRangeOverrides:
MutableMap<String, MutableList<MavenVersionRangeOverride>> = LinkedHashMap()

/** Declares the version replacements to apply to dependencies of [group]. Matched exactly. */
fun dependency(group: String, action: Action<in MavenDependencyOverride>) {
val dependencyOverride = MavenDependencyOverride().also(action::execute)
addDependencyOverride(group, dependencyOverride)
}

/** Groovy DSL overload of [dependency], so assignments delegate to the override. */
fun dependency(group: String, closure: Closure<*>) {
val dependencyOverride = MavenDependencyOverride()
closure.delegate = dependencyOverride
closure.resolveStrategy = Closure.DELEGATE_FIRST
closure.call(dependencyOverride)
addDependencyOverride(group, dependencyOverride)
}

private fun addDependencyOverride(
group: String,
dependencyOverride: MavenDependencyOverride
) {
require(
dependencyOverride.matchVersions.isNotEmpty() ||
dependencyOverride.matchPattern.isNotBlank() ||
dependencyOverride.matchVersionRanges.isNotEmpty()
) {
"At least one matchVersions, matchPattern, or matchVersionRanges entry is required " +
"for dependency group '$group'"
}

require(dependencyOverride.replacement.isNotBlank()) {
"A replacement is required for dependency group '$group'"
}

require("*" !in dependencyOverride.matchVersions) {
"'*' is not supported in matchVersions for dependency group '$group'; " +
"use matchVersionRanges for concrete versions"
}

val versionScheme = GenericVersionScheme()
dependencyOverride.matchVersionRanges.forEach { range ->
try {
versionScheme.parseVersionConstraint(range)
} catch (e: InvalidVersionSpecificationException) {
throw IllegalArgumentException(
"Invalid matchVersionRanges entry '$range' for dependency group '$group'",
e
)
}
}

if (dependencyOverride.matchVersions.isNotEmpty() || dependencyOverride.matchPattern.isNotBlank()) {
val groupOverrides = dependencyVersionOverrides.getOrPut(group, ::LinkedHashMap)
dependencyOverride.matchVersions.forEach { groupOverrides[it] = dependencyOverride.replacement }
if (dependencyOverride.matchPattern.isNotBlank()) {
groupOverrides[dependencyOverride.matchPattern] = dependencyOverride.replacement
}
}
if (dependencyOverride.matchVersionRanges.isNotEmpty()) {
val groupOverrides = dependencyVersionRangeOverrides.getOrPut(group, ::ArrayList)
dependencyOverride.matchVersionRanges.forEach { range ->
groupOverrides.add(MavenVersionRangeOverride(range, dependencyOverride.replacement))
}
}
}

/** Checks the block is complete, once the whole `mavenPomOverrides { }` closure has been evaluated. */
internal fun validate() {
require(artifactVersions.isNotBlank()) {
"artifactVersions is required in mavenPomOverrides, e.g. artifactVersions = \"[7.4,8)\", " +
"or \"[,)\" to cover every version of the directive"
}

try {
GenericVersionScheme().parseVersionConstraint(artifactVersions)
} catch (e: InvalidVersionSpecificationException) {
throw IllegalArgumentException(
"Invalid artifactVersions Maven version range '$artifactVersions' in mavenPomOverrides",
e
)
}

require(dependencyVersionOverrides.isNotEmpty() || dependencyVersionRangeOverrides.isNotEmpty()) {
"At least one dependency(group) { } override is required in mavenPomOverrides"
}
}
}

/** Version replacement values for a dependency group in [MavenPomOverrides]. */
open class MavenDependencyOverride : Serializable {
/**
* Concrete version strings to replace, matched exactly, for example `"9.4.59"`.
*
* Use [matchPattern] for raw non-version text such as an unexpanded Maven property placeholder,
* or [matchVersionRanges] to select concrete versions with a Maven version range.
*/
var matchVersions: MutableList<String> = ArrayList()

/** Raw POM version text to replace literally, for example `'${jetty.version}'`. */
var matchPattern: String = ""

/** Maven version ranges to replace, for example `"[9.4.59,9.5)"`. */
var matchVersionRanges: MutableList<String> = ArrayList()

/** Version to write into the POM in place of every match. Required. */
var replacement: String = ""
}

internal data class MavenVersionRangeOverride(
val range: String,
val replacement: String
) : Serializable
Loading
Loading