From d82e429159aab740289d7b3fdd144caa69f5e005 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 8 Oct 2026 14:49:22 -0400 Subject: [PATCH 1/2] Reuse Maven resolution across Muzzle modules --- .../gradle/plugin/muzzle/MuzzlePlugin.kt | 23 ++++++-- .../muzzle/MuzzlePluginFunctionalTest.kt | 56 +++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt index dea0c63e7cf..796628c6245 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt @@ -103,11 +103,8 @@ class MuzzlePlugin : Plugin { val validationTasks = MuzzleValidationTasks(project.providers) { val startTime = System.currentTimeMillis() - val system = MuzzleMavenRepoUtils.newRepositorySystem() - val session = MuzzleMavenRepoUtils.newRepositorySystemSession(system) - val taskPlanner = MuzzleTaskPlanner.from(system, session) val directives = project.extensions.getByType().directives - val checks = taskPlanner.plan(directives).map { plan -> + val checks = sharedTaskPlanner(project).plan(directives).map { plan -> val check = registerMuzzleTask(plan.directive, plan.artifact, project, muzzleBootstrap, muzzleTooling) project.logger.info("configured ${plan.directive}") check @@ -142,6 +139,24 @@ class MuzzlePlugin : Plugin { } } + private fun sharedTaskPlanner(project: Project): MuzzleTaskPlanner { + val properties = project.rootProject.extensions.extraProperties + val key = "datadogMuzzleTaskPlanners" + if (!properties.has(key)) { + properties.set(key, mutableMapOf>>, MuzzleTaskPlanner>()) + } + @Suppress("UNCHECKED_CAST") + val planners = properties.get(key) as MutableMap>>, MuzzleTaskPlanner> + // Repository IDs can be reused for different URLs, so keep their local metadata separate. + val repositories = project.extensions.getByType().directives + .map { it.additionalRepositories.toList() }.distinct() + return planners.getOrPut(repositories) { + val system = MuzzleMavenRepoUtils.newRepositorySystem() + val session = MuzzleMavenRepoUtils.newRepositorySystemSession(system) + MuzzleTaskPlanner.from(system, session) + } + } + companion object { /** * Registers a new muzzle task for the given directive and artifact. diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index 75107a2f41a..33488fc7fbb 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -1,5 +1,6 @@ package datadog.gradle.plugin.muzzle +import datadog.gradle.plugin.MavenRepoFixture import org.assertj.core.api.Assertions.assertThat import org.gradle.testkit.runner.TaskOutcome.SUCCESS import org.junit.jupiter.api.Test @@ -31,6 +32,61 @@ class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() { assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome).isEqualTo(SUCCESS) } + @Test + fun `planning sees directives configured after project evaluation`() { + writeProject( + """ + plugins { + id("java") + id("dd-trace-java.muzzle") + } + """ + ) + writeNoopScanPlugin() + writeRootProject( + """ + plugins { id("dd-trace-java.muzzle") apply false } + gradle.projectsEvaluated { + project(":dd-java-agent:instrumentation:demo").extensions.configure { + pass { coreJdk() } + } + } + """ + ) + val result = run(":dd-java-agent:instrumentation:demo:muzzle") + assertThat(result.output).contains("BUILD SUCCESSFUL") + assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome).isEqualTo(SUCCESS) + } + + @Test + fun `shared planning keeps module repositories separate`() { + val empty = createMavenRepoFixture() + val first = MavenRepoFixture(projectDir.resolve("first-repository")) + val second = MavenRepoFixture(projectDir.resolve("second-repository")) + first.publishVersions("com.example.test", "shared-lib", listOf("1.0.0")) + second.publishVersions("com.example.test", "shared-lib", listOf("2.0.0")) + fun script(url: String) = """ + plugins { id("java"); id("dd-trace-java.muzzle") } + repositories { maven { url = uri("$url") } } + muzzle { + extraRepository("fixture", "$url") + pass { group = "com.example.test"; module = "shared-lib"; versions = "[1.0,3.0)" } + } + """ + writeProject(script(first.repoUrl)) + addSubproject("dd-java-agent:instrumentation:other", script(second.repoUrl)) + writeNoopScanPlugin() + + val result = run("muzzle", env = mapOf("MAVEN_REPOSITORY_PROXY" to empty.repoUrl)) + + assertThat(result.output).contains("BUILD SUCCESSFUL") + for ((module, version) in listOf("demo" to "1.0.0", "other" to "2.0.0")) { + val prefix = ":dd-java-agent:instrumentation:$module:muzzle-AssertPass-com.example.test-shared-lib-" + assertThat(result.task("$prefix$version")?.outcome).isEqualTo(SUCCESS) + assertThat(result.tasks.filter { it.path.startsWith(prefix) }).hasSize(1) + } + } + @ParameterizedTest @ValueSource(strings = ["muzzle", ":dd-java-agent:instrumentation:demo:muzzle", "runMuzzle", "verifyCompatibility"]) fun `runs validation through different task entry points`(taskName: String) { From c9b1760f4531e48e24d7e03471143b2484f0a904 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 15 Oct 2026 16:56:02 -0500 Subject: [PATCH 2/2] Add a separate MuzzleTaskPlannerService --- .../gradle/plugin/muzzle/MuzzlePlugin.kt | 24 ++------ .../planner/MuzzleTaskPlannerService.kt | 26 ++++++++ .../muzzle/MuzzlePluginFunctionalTest.kt | 59 +++++++++++++++++++ 3 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerService.kt diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt index 796628c6245..fab305dc73b 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt @@ -1,10 +1,10 @@ package datadog.gradle.plugin.muzzle +import datadog.gradle.plugin.muzzle.planner.MuzzleTaskPlannerService 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 org.eclipse.aether.artifact.Artifact import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin @@ -104,7 +104,9 @@ class MuzzlePlugin : Plugin { val validationTasks = MuzzleValidationTasks(project.providers) { val startTime = System.currentTimeMillis() val directives = project.extensions.getByType().directives - val checks = sharedTaskPlanner(project).plan(directives).map { plan -> + val planner = project.gradle.sharedServices + .registerIfAbsent("datadogMuzzleTaskPlanners", MuzzleTaskPlannerService::class.java) + val checks = planner.get().plan(directives).map { plan -> val check = registerMuzzleTask(plan.directive, plan.artifact, project, muzzleBootstrap, muzzleTooling) project.logger.info("configured ${plan.directive}") check @@ -139,24 +141,6 @@ class MuzzlePlugin : Plugin { } } - private fun sharedTaskPlanner(project: Project): MuzzleTaskPlanner { - val properties = project.rootProject.extensions.extraProperties - val key = "datadogMuzzleTaskPlanners" - if (!properties.has(key)) { - properties.set(key, mutableMapOf>>, MuzzleTaskPlanner>()) - } - @Suppress("UNCHECKED_CAST") - val planners = properties.get(key) as MutableMap>>, MuzzleTaskPlanner> - // Repository IDs can be reused for different URLs, so keep their local metadata separate. - val repositories = project.extensions.getByType().directives - .map { it.additionalRepositories.toList() }.distinct() - return planners.getOrPut(repositories) { - val system = MuzzleMavenRepoUtils.newRepositorySystem() - val session = MuzzleMavenRepoUtils.newRepositorySystemSession(system) - MuzzleTaskPlanner.from(system, session) - } - } - companion object { /** * Registers a new muzzle task for the given directive and artifact. diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerService.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerService.kt new file mode 100644 index 00000000000..dde7279673f --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/planner/MuzzleTaskPlannerService.kt @@ -0,0 +1,26 @@ +package datadog.gradle.plugin.muzzle.planner + +import datadog.gradle.plugin.muzzle.MuzzleDirective +import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +/** Shares Maven planners within a build while isolating modules with different repository settings. */ +internal abstract class MuzzleTaskPlannerService : BuildService { + private data class RepositorySettings(val repositories: List>>) + + private val planners = mutableMapOf() + + /** Serializes cache access and planning so callers never use a Maven session concurrently. */ + @Synchronized + fun plan(directives: List): List { + // Repository IDs can be reused for different URLs, so keep their local metadata separate. + val settings = RepositorySettings(directives.map { it.additionalRepositories.toList() }.distinct()) + val planner = planners.getOrPut(settings) { + val system = MuzzleMavenRepoUtils.newRepositorySystem() + val session = MuzzleMavenRepoUtils.newRepositorySystemSession(system) + MuzzleTaskPlanner.from(system, session) + } + return planner.plan(directives) + } +} diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index 33488fc7fbb..111ac271b87 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -1,11 +1,14 @@ package datadog.gradle.plugin.muzzle +import com.sun.net.httpserver.HttpServer import datadog.gradle.plugin.MavenRepoFixture import org.assertj.core.api.Assertions.assertThat import org.gradle.testkit.runner.TaskOutcome.SUCCESS import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource +import java.net.InetSocketAddress +import java.util.concurrent.atomic.AtomicInteger import kotlin.io.path.readText class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() { @@ -58,6 +61,62 @@ class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() { assertThat(result.task(":dd-java-agent:instrumentation:demo:muzzle-AssertPass-core-jdk")?.outcome).isEqualTo(SUCCESS) } + @Test + fun `matching modules share Maven metadata within each build`() { + val repository = createMavenRepoFixture() + repository.publishVersions("com.example.test", "shared-lib", listOf("1.0.0", "2.0.0")) + val metadataRequests = AtomicInteger() + val server = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0) + server.createContext("/") { exchange -> + try { + val path = exchange.requestURI.path.removePrefix("/") + if (path == "com/example/test/shared-lib/maven-metadata.xml") { + metadataRequests.incrementAndGet() + } + val file = repository.repoDir.resolve(path) + if (file.isFile) { + val content = file.readBytes() + exchange.sendResponseHeaders(200, content.size.toLong()) + exchange.responseBody.write(content) + } else { + exchange.sendResponseHeaders(404, -1) + } + } finally { + exchange.close() + } + } + server.start() + try { + val script = """ + plugins { id("java"); id("dd-trace-java.muzzle") } + muzzle { + pass { group = "com.example.test"; module = "shared-lib"; versions = "[1.0,3.0)" } + } + """ + writeProject(script) + addSubproject("dd-java-agent:instrumentation:other", script) + val env = mapOf("MAVEN_REPOSITORY_PROXY" to "http://127.0.0.1:${server.address.port}/") + + val unrelated = run(":dd-java-agent:instrumentation:demo:tasks", "--all", env = env) + assertThat(unrelated.output).contains("BUILD SUCCESSFUL").doesNotContain("muzzle-end") + assertThat(metadataRequests.get()).isZero() + + repeat(2) { build -> + val result = run("muzzle", "--dry-run", "--parallel", env = env) + assertThat(result.output).contains("BUILD SUCCESSFUL") + for (module in listOf("demo", "other")) { + for (version in listOf("1.0.0", "2.0.0")) { + assertThat(result.output) + .contains(":dd-java-agent:instrumentation:$module:muzzle-AssertPass-com.example.test-shared-lib-$version SKIPPED") + } + } + assertThat(metadataRequests.get()).isEqualTo(build + 1) + } + } finally { + server.stop(0) + } + } + @Test fun `shared planning keeps module repositories separate`() { val empty = createMavenRepoFixture()