Skip to content
Draft
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
Expand Up @@ -83,6 +83,16 @@ open class MuzzleDirective : Serializable {
}
}

internal fun inverse(version: String): MuzzleDirective = MuzzleDirective().apply {
name = this@MuzzleDirective.name
group = this@MuzzleDirective.group
module = this@MuzzleDirective.module
versions = version
assertPass = !this@MuzzleDirective.assertPass
excludedDependencies = this@MuzzleDirective.excludedDependencies
includeSnapshots = this@MuzzleDirective.includeSnapshots
}

/**
* Slug of directive name.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.eclipse.aether.version.Version
import org.gradle.api.GradleException
import org.gradle.api.logging.Logging
import java.nio.file.Files
import kotlin.random.Random

internal object MuzzleMavenRepoUtils {
private val log = Logging.getLogger(MuzzleMavenRepoUtils::class.java)
Expand Down Expand Up @@ -82,7 +83,8 @@ internal object MuzzleMavenRepoUtils {
muzzleDirective: MuzzleDirective,
system: RepositorySystem,
session: RepositorySystemSession,
defaultRepos: List<RemoteRepository> = defaultMuzzleRepos()
defaultRepos: List<RemoteRepository> = defaultMuzzleRepos(),
random: Random = Random.Default
): Set<MuzzleDirective> {
val allVersionsArtifact = DefaultArtifact(
muzzleDirective.group,
Expand Down Expand Up @@ -114,17 +116,10 @@ internal object MuzzleMavenRepoUtils {
return MuzzleVersionUtils.filterAndLimitVersions(
allRangeResult,
muzzleDirective.skipVersions,
muzzleDirective.includeSnapshots
muzzleDirective.includeSnapshots,
random
).map { version ->
MuzzleDirective().apply {
name = muzzleDirective.name
group = muzzleDirective.group
module = muzzleDirective.module
versions = version.toString()
assertPass = !muzzleDirective.assertPass
excludedDependencies = muzzleDirective.excludedDependencies
includeSnapshots = muzzleDirective.includeSnapshots
}
muzzleDirective.inverse(version.toString())
}.toSet()
}

Expand Down Expand Up @@ -373,12 +368,14 @@ internal object MuzzleMavenRepoUtils {
*/
fun muzzleDirectiveToArtifacts(
muzzleDirective: MuzzleDirective,
rangeResult: VersionRangeResult
rangeResult: VersionRangeResult,
random: Random = Random.Default
): Set<Artifact> {
val versions = MuzzleVersionUtils.filterAndLimitVersions(
rangeResult,
muzzleDirective.skipVersions,
muzzleDirective.includeSnapshots
muzzleDirective.includeSnapshots,
random
)
val allVersionArtifacts = versions.map { version ->
DefaultArtifact(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
package datadog.gradle.plugin.muzzle

import datadog.gradle.plugin.muzzle.planner.MavenMuzzleResolutionService
import datadog.gradle.plugin.muzzle.planner.MuzzlePlannedVersion
import datadog.gradle.plugin.muzzle.planner.MuzzlePlanningRequest
import datadog.gradle.plugin.muzzle.planner.MuzzlePlansValueSource
import datadog.gradle.plugin.muzzle.planner.MuzzleTaskPlanner
import datadog.gradle.plugin.muzzle.tasks.MuzzleEndTask
import datadog.gradle.plugin.muzzle.tasks.MuzzleGenerateReportTask
import datadog.gradle.plugin.muzzle.tasks.MuzzleGetReferencesTask
import datadog.gradle.plugin.muzzle.tasks.MuzzleTask
import datadog.gradle.plugin.muzzle.planner.MuzzleTaskPlanner
import kotlin.random.Random
import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.artifact.DefaultArtifact
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.configuration.BuildFeatures
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.exclude
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.named
import org.gradle.kotlin.dsl.project
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.withType
import javax.inject.Inject

/**
* muzzle task plugin which runs muzzle validation against a range of dependencies.
*/
class MuzzlePlugin : Plugin<Project> {
class MuzzlePlugin @Inject constructor(private val buildFeatures: BuildFeatures) : Plugin<Project> {
/**
* Applies the Muzzle plugin to the given project.
*
Expand Down Expand Up @@ -67,6 +77,7 @@ class MuzzlePlugin : Plugin<Project> {

// compileMuzzle compiles all projects required to run muzzle validation.
// Not adding group and description to keep this task from showing in `gradle tasks`.
val extension = project.extensions.getByType<MuzzleExtension>()
val compileMuzzle = project.tasks.register("compileMuzzle") {
inputs.files(project.providers.provider { project.allMainSourceSet.map { it.output } })
dependsOn(bootstrapProject.tasks.named("compileJava"))
Expand All @@ -76,23 +87,29 @@ class MuzzlePlugin : Plugin<Project> {

val muzzleTask = project.tasks.register<MuzzleTask>("muzzle") {
description = "Check instrumentation compatibility against the configured dependency versions"
this.muzzleBootstrap.set(muzzleBootstrap)
this.muzzleTooling.set(muzzleTooling)
this.muzzleBootstrap.from(muzzleBootstrap)
this.muzzleTooling.from(muzzleTooling)
dependsOn(compileMuzzle)
}

project.tasks.withType<MuzzleTask>().configureEach {
agentClassPath.from(project.providers.provider { project.allMainSourceSet.map { it.runtimeClasspath } })
muzzleClassPath.from(project.configurations.named(if (name == "muzzle") "compileClasspath" else name))
}

project.tasks.register<MuzzleGetReferencesTask>("printReferences") {
dependsOn(compileMuzzle)
classpath.from(project.mainSourceSet.runtimeClasspath)
}.also {
val referenceOutput = it.flatMap { task -> task.outputFile }
val printReferencesTask = project.tasks.register("actuallyPrintReferences") {
doLast {
println(it.get().outputFile.get().asFile.readText())
println(referenceOutput.get().asFile.readText())
}
}
it.configure { finalizedBy(printReferencesTask) }
}

val extension = project.extensions.getByType<MuzzleExtension>()
val runtimeClasspath = project.mainSourceSet.runtimeClasspath
val report = project.tasks.register<MuzzleGenerateReportTask>("generateMuzzleReport") {
reportDirectives.convention(project.providers.provider { extension.directives })
Expand All @@ -102,11 +119,22 @@ class MuzzlePlugin : Plugin<Project> {
project.publishMuzzleReport(report.flatMap { it.versionsFile })

val validationTasks = MuzzleValidationTasks(project.providers) {
val startTime = System.currentTimeMillis()
val directives = project.extensions.getByType<MuzzleExtension>().directives
val checks = sharedTaskPlanner(project).plan(directives).map { plan ->
val check = registerMuzzleTask(plan.directive, plan.artifact, project, muzzleBootstrap, muzzleTooling)
project.logger.info("configured ${plan.directive}")
val directives = extension.directives.toList()
muzzleTask.configure {
checkCompileTimeDependencies.set(!directives.any { it.assertPass })
}
val checks = muzzlePlans(project, directives).map { plan ->
val original = directives[plan.directiveIndex]
val directive = if (plan.assertPass == original.assertPass) {
original
} else {
original.inverse(requireNotNull(plan.version))
}
val artifact = plan.version?.let {
DefaultArtifact(directive.group, directive.module, directive.classifier ?: "", "jar", it)
}
val check = registerMuzzleTask(directive, artifact, project, muzzleBootstrap, muzzleTooling)
project.logger.info("configured $directive")
check
}

Expand All @@ -115,30 +143,37 @@ class MuzzlePlugin : Plugin<Project> {
} else {
checks
}

val timingTask = project.tasks.register<MuzzleEndTask>("muzzle-end") {
startTimeMs.set(startTime)
sourceFile.set(
project.projectDir
.relativeTo(project.rootProject.projectDir)
.invariantSeparatorsPath
)
sourceFile.set(project.projectDir.relativeTo(project.rootProject.projectDir).invariantSeparatorsPath)
muzzleResultFiles.from(muzzleReportTasks.map { it.flatMap { task -> task.result } })
}

val executionChain: List<TaskProvider<out Task>> = checks + timingTask
executionChain.zipWithNext { current, next ->
current.configure { finalizedBy(next) }
}

// Note this is a set, but only the first task of the chain is passed.
setOf(executionChain.first().get())
}
muzzleTask.configure {
finalizedBy(validationTasks.tasks)
}
}

private fun muzzlePlans(project: Project, directives: List<MuzzleDirective>): List<MuzzlePlannedVersion> {
if (buildFeatures.configurationCache.active.get()) {
// Only selected modules become cache inputs; revalidate their coordinates on cache reuse.
return project.providers.of(MuzzlePlansValueSource::class.java) {
parameters.requests.set(listOf(MuzzlePlanningRequest(project.path, directives)))
parameters.samplingSeed.set(Random.nextLong())
}.get()
}
val planner = sharedTaskPlanner(project)
return directives.flatMapIndexed { index, directive ->
planner.plan(listOf(directive)).map { plan ->
MuzzlePlannedVersion(project.path, index, plan.artifact?.version, plan.directive.assertPass)
}
}
}

private fun sharedTaskPlanner(project: Project): MuzzleTaskPlanner {
val properties = project.rootProject.extensions.extraProperties
val key = "datadogMuzzleTaskPlanners"
Expand All @@ -153,7 +188,7 @@ class MuzzlePlugin : Plugin<Project> {
return planners.getOrPut(repositories) {
val system = MuzzleMavenRepoUtils.newRepositorySystem()
val session = MuzzleMavenRepoUtils.newRepositorySystemSession(system)
MuzzleTaskPlanner.from(system, session)
MuzzleTaskPlanner(MavenMuzzleResolutionService(system, session))
}
}

Expand Down Expand Up @@ -238,8 +273,8 @@ class MuzzlePlugin : Plugin<Project> {

val muzzleTask = instrumentationProject.tasks.register<MuzzleTask>(muzzleTaskName) {
this.muzzleDirective.set(muzzleDirective)
this.muzzleBootstrap.set(muzzleBootstrap)
this.muzzleTooling.set(muzzleTooling)
this.muzzleBootstrap.from(muzzleBootstrap)
this.muzzleTooling.from(muzzleTooling)
}

return muzzleTask
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package datadog.gradle.plugin.muzzle
import org.eclipse.aether.resolution.VersionRangeResult
import org.eclipse.aether.version.Version
import java.util.Locale
import kotlin.random.Random

internal object MuzzleVersionUtils {
private val END_NMN_PATTERN = Regex("^.*\\.[0-9]+[mM][0-9]+$")
Expand All @@ -19,10 +20,11 @@ internal object MuzzleVersionUtils {
fun filterAndLimitVersions(
result: VersionRangeResult,
skipVersions: Set<String>,
includeSnapshots: Boolean
includeSnapshots: Boolean,
random: Random = Random.Default
): Set<Version> {
val filtered = filterVersion(result.versions.toSet(), skipVersions, includeSnapshots)
return limitLargeRanges(result, filtered, skipVersions)
return limitLargeRanges(result, filtered, skipVersions, random)
}

/**
Expand Down Expand Up @@ -81,15 +83,16 @@ internal object MuzzleVersionUtils {
private fun limitLargeRanges(
result: VersionRangeResult,
versions: Set<Version>,
skipVersions: Set<String>
skipVersions: Set<String>,
random: Random
): Set<Version> {
if (versions.size <= 1) return versions
val beforeSize = versions.size
val filteredVersions = versions.toMutableList().apply {
removeAll { skipVersions.contains(it.toString()) }
}
val versionSet = VersionSet(filteredVersions)
val shuffled = versionSet.lowAndHighForMajorMinor.shuffled().toMutableList()
val shuffled = versionSet.lowAndHighForMajorMinor.shuffled(random).toMutableList()
var afterSize = shuffled.size
while (RANGE_COUNT_LIMIT <= afterSize) {
val version = shuffled.removeAt(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils
import org.eclipse.aether.RepositorySystem
import org.eclipse.aether.RepositorySystemSession
import org.eclipse.aether.artifact.Artifact
import kotlin.random.Random

/**
* Default [MuzzleResolutionService] implementation backed by Maven/Aether resolution.
*/
internal class MavenMuzzleResolutionService(
private val system: RepositorySystem,
private val session: RepositorySystemSession,
private val random: Random = Random.Default,
) : MuzzleResolutionService {
override fun resolveArtifacts(directive: MuzzleDirective): Set<Artifact> {
val range = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, session)
return MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts(directive, range)
return MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts(directive, range, random)
}

override fun inverseOf(directive: MuzzleDirective): Set<MuzzleDirective> =
MuzzleMavenRepoUtils.inverseOf(directive, system, session)
MuzzleMavenRepoUtils.inverseOf(directive, system, session, random = random)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package datadog.gradle.plugin.muzzle.planner

import datadog.gradle.plugin.muzzle.MuzzleDirective
import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils
import org.eclipse.aether.DefaultRepositorySystemSession
import org.gradle.api.Describable
import org.gradle.api.logging.Logging
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import java.io.Serializable
import kotlin.random.Random

/** Tracks the selected coordinates as configuration inputs. */
internal abstract class MuzzlePlansValueSource :
ValueSource<List<MuzzlePlannedVersion>, MuzzlePlansValueSource.Parameters>,
Describable {
interface Parameters : ValueSourceParameters {
val requests: ListProperty<MuzzlePlanningRequest>
val samplingSeed: Property<Long>
}

override fun getDisplayName() = "Muzzle coordinates"

override fun obtain(): List<MuzzlePlannedVersion> {
val startNanos = System.nanoTime()
val system = MuzzleMavenRepoUtils.newRepositorySystem()
val random = Random(parameters.samplingSeed.get())
val requests = parameters.requests.get()
val planners = mutableMapOf<List<List<Triple<String, String, String>>>, MuzzleTaskPlanner>()
val plans = requests.flatMap { request ->
// Repository IDs can be reused for different URLs, so keep their local metadata separate.
val repositories = request.directives.map { it.additionalRepositories.toList() }.distinct()
val planner = planners.getOrPut(repositories) {
val session = DefaultRepositorySystemSession(MuzzleMavenRepoUtils.newRepositorySystemSession(system)).apply {
// Background lookups escape Gradle's ValueSource input tracking and expose temporary files.
setConfigProperty("aether.metadataResolver.threads", 1)
}
MuzzleTaskPlanner(MavenMuzzleResolutionService(system, session, random = random))
}
request.directives.flatMapIndexed { index, directive ->
planner.plan(listOf(directive)).map { plan ->
MuzzlePlannedVersion(request.projectPath, index, plan.artifact?.version, plan.directive.assertPass)
}
}
}
Logging.getLogger(MuzzlePlansValueSource::class.java).info(
"Muzzle planned ${plans.size} checks for ${requests.size} modules in " +
"${(System.nanoTime() - startNanos) / 1_000_000}ms"
)
return plans
}
}

internal data class MuzzlePlanningRequest(
val projectPath: String,
val directives: List<MuzzleDirective>
) : Serializable

internal data class MuzzlePlannedVersion(
val projectPath: String,
val directiveIndex: Int,
val version: String?,
val assertPass: Boolean
) : Serializable
Loading
Loading