From ef6bff8eec36aa33165e169470c65875fb64023a Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 3 Jul 2026 09:58:50 +0200 Subject: [PATCH 1/5] fix(build): Stop publishing extra source-set output twice The `TracerJavaExtension.addSourceSetFor` added the extra source-set output to the `implementation` configuration. A project dependency on the runtime classpath resolves the producer's `runtimeElements` variant. That variant publishes the project jar and runtime dependencies derived from `implementation`, so consumers (the instrumentation project in particular) received the same classes twice: 1. once from the jar configured with `jar.from(mainForJavaVersionSourceSet.output)`, 2. once from the raw class directory published as a dependency This change keeps the additional source set local to the producer module's _compile_ and _test_ classpaths respectively with `compileOnly` and `testImplementation`. The jar remains the runtime artifact consumed by downstream aggregating projects, and it still contains the additional source-set classes. What was considered: dropping the `jar.from(output)` config, it would remove the duplicate, but it would make the consumed module jar incomplete and leave consuming projects relying on class directories instead of the artifact. This would also a the side effect of making Gradle to track individual classes for up-to-date tracking, which consumes more memory. This keeps the existing "jar contract" and only prevent publishing the additional source-set output through `runtimeElements`. This removes the exception-profiling and instrumentation duplicate exclusions that were needed in the instrumentation `shadowJar`. --- dd-java-agent/instrumentation/build.gradle | 7 +++--- gradle/java_no_deps.gradle | 26 +++++++++++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/dd-java-agent/instrumentation/build.gradle b/dd-java-agent/instrumentation/build.gradle index 8077740656f..05309a51a00 100644 --- a/dd-java-agent/instrumentation/build.gradle +++ b/dd-java-agent/instrumentation/build.gradle @@ -137,10 +137,9 @@ tasks.named('shadowJar', ShadowJar) { 'datadog/trace/agent/tooling/bytebuddy/DDJava9ClassFileTransformer*.class', // agent-installer also publishes this Java 25 source-set output at runtime. 'datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterFFM*.class', - // aggregate instrumentation includes some classes directly and via jars. - 'datadog/trace/instrumentation/**/*.class', - // same aggregate duplication, for Java 11 exception profiling advice. - 'datadog/exceptions/instrumentation/**/*.class', + // jakarta-servlet-5.0 publishes the relocated advice jar as a runtime + // file dependency while also folding those classes into its own jar. + 'datadog/trace/instrumentation/servlet5/**/*.class', ]) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } diff --git a/gradle/java_no_deps.gradle b/gradle/java_no_deps.gradle index 955fcf65659..eb7d87e658f 100644 --- a/gradle/java_no_deps.gradle +++ b/gradle/java_no_deps.gradle @@ -103,13 +103,33 @@ class TracerJavaExtension { } } - // "socket-utils" is only set to compileOnly because the implementation dependency incorrectly adds Java17 classes to all jar prefixes. - // This causes the AgentJarIndex to search for other non-Java17 classes in the wrong prefix location and fail to resolve class names. if (sourceSetConfig.compileOnly.orElse(false).get()) { + // The ":utils:socket-utils" is only set to `compileOnly` because the `implementation` dependency + // adds Java17 classes to all jar prefixes, which is incorrect for this project. + // This causes the AgentJarIndex to search for other non-Java17 classes in the wrong prefix + // location and to fail to resolve class names. project.dependencies.add("compileOnly", mainForJavaVersionSourceSet.output) } else { + // Wire this additional source-set into this project without publishing its raw output + // (handled by the jar configuration). The following + // + // * make this additional source-set's _compile classpath_ visible to **main compilation** through `compileOnly` + // * make this additional source-set's _compiled output_ visible to **main compilation** through `compileOnly` + // * make this additional source-set's _compiled output_ visible to tests through `testImplementation` + // + // The jar task produces the artifact that consumer wants to see, and it is configured + // below to add classes of this additional source-set. + // + // NOTE: + // We don't want to add this source-set's output to the `implementation` configuration! + // Otherwise this additional source-set output directory (where classes are) would end up + // being published as a runtime dependency through `runtimeElements`. + // The `runtimeElements` configuration publishes the jar artifact by default ; thus + // `runtimeElements` would have both the jar, and the added output folder. + // In short this makes classes of this extra source-set in both the jar and the directory. project.dependencies.add("compileOnly", project.files(mainForJavaVersionSourceSet.compileClasspath)) - project.dependencies.add("implementation", mainForJavaVersionSourceSet.output) + project.dependencies.add("compileOnly", mainForJavaVersionSourceSet.output) + project.dependencies.add("testImplementation", mainForJavaVersionSourceSet.output) } project.tasks.named("jar", Jar) { From 42247c9a2bac294c893c6b6f6d85aa0318f01b0b Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 3 Jul 2026 10:01:33 +0200 Subject: [PATCH 2/5] fix(build): Stop publishing agent-installer source-set output twice The `agent-installer` project added its Java-version-specific source-set outputs to the `runtimeOnly` configuration. A project dependency on the runtime classpath resolves the producer's `runtimeElements` variant. That variant publishes the project jar and runtime dependencies derived from `runtimeOnly`, so consumers (the instrumentation project in particular) received the same classes twice: 1. once from the jar configured with `from sourceSets.main_java11.output` and `from sourceSets.main_java25.output`, 2. once from the raw class directories published as runtime dependencies This change keeps the Java-version-specific source sets local to the producer module's test classpath with `testImplementation`. The jar remains the runtime artifact consumed by downstream aggregating projects, and it still contains the Java 11 and Java 25 source-set classes. Dropping the jar configuration would remove the duplicate, but it would make the consumed `agent-installer` jar incomplete and leave consuming projects relying on class directories instead of the artifact. This keeps the existing "jar contract" and only prevents publishing the Java-version-specific source-set output through `runtimeElements`. This removes the agent-installer duplicate exclusions that were needed in the instrumentation `shadowJar`. --- dd-java-agent/agent-installer/build.gradle | 18 ++++++++++++++++-- dd-java-agent/instrumentation/build.gradle | 4 ---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/dd-java-agent/agent-installer/build.gradle b/dd-java-agent/agent-installer/build.gradle index b8c8aac5460..766e55af799 100644 --- a/dd-java-agent/agent-installer/build.gradle +++ b/dd-java-agent/agent-installer/build.gradle @@ -42,12 +42,26 @@ tasks.named("compileMain_java25Java", JavaCompile) { } dependencies { + // Wire the Java-version-specific source sets into this project without publishing + // their raw output: + // + // - each additional source set compiles against the main output and only the + // dependencies it needs + // - their compiled outputs stay visible to tests through `testImplementation` + // - their compiled outputs are folded into this project's jar by the jar task below + // + // Main code loads these classes reflectively at agent runtime, so downstream + // consumers should get them from the agent-installer jar. Do not add these + // outputs to `runtimeOnly`: `runtimeElements` publishes both the jar artifact + // and runtime dependencies. Publishing the raw outputs there would expose the + // same classes twice, once from the jar and once from the class directories. main_java11CompileOnly project(':dd-java-agent:agent-tooling') main_java11CompileOnly sourceSets.main.output + testImplementation sourceSets.main_java11.output + main_java25CompileOnly project(':dd-trace-core') main_java25CompileOnly sourceSets.main.output - runtimeOnly sourceSets.main_java11.output - runtimeOnly sourceSets.main_java25.output + testImplementation sourceSets.main_java25.output } tasks.named("jar", Jar) { diff --git a/dd-java-agent/instrumentation/build.gradle b/dd-java-agent/instrumentation/build.gradle index 05309a51a00..6e59305d967 100644 --- a/dd-java-agent/instrumentation/build.gradle +++ b/dd-java-agent/instrumentation/build.gradle @@ -133,10 +133,6 @@ tasks.named('shadowJar', ShadowJar) { // // Keeping `duplicatesStrategy = FAIL` only, Shadow 9 now fails while creating the instrumentation jar. filesMatching([ - // agent-installer also publishes this Java 11 source-set output at runtime. - 'datadog/trace/agent/tooling/bytebuddy/DDJava9ClassFileTransformer*.class', - // agent-installer also publishes this Java 25 source-set output at runtime. - 'datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterFFM*.class', // jakarta-servlet-5.0 publishes the relocated advice jar as a runtime // file dependency while also folding those classes into its own jar. 'datadog/trace/instrumentation/servlet5/**/*.class', From 9133955115dc1321eb7c4d2464fbd84c5baceeef Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 3 Jul 2026 10:04:06 +0200 Subject: [PATCH 3/5] fix(build): Keep servlet5 relocated jar off `runtimeElements` The `jakarta-servlet-5.0` project added the relocated javax-to-jakarta advice jar to the `implementation` configuration. And a project dependency on the runtime classpath resolves the producer's `runtimeElements` variant. That variant publishes the project jar and its runtime dependencies that are derived from `implementation`, so consumers (the instrumentation project in particular) received the same classes twice: 1. once from the module jar configured with `from zipTree(relocatedJavaxJar.outputs.files.asPath)`, 2. once from the relocated jar published as a runtime dependency This change keeps the relocated jar local to the producer module's _compile_ and _test_ classpaths with `compileOnly` and `testImplementation` respectively. The module jar remains the runtime artifact consumed by downstream projects, and it still contains the relocated servlet5 advice classes. With this, the last duplicate is gone, so drop the remaining `filesMatching { EXCLUDE }` workaround and refresh the comment. --- dd-java-agent/instrumentation/build.gradle | 25 ++++++------------- .../servlet/jakarta-servlet-5.0/build.gradle | 16 +++++++++++- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/dd-java-agent/instrumentation/build.gradle b/dd-java-agent/instrumentation/build.gradle index 6e59305d967..a3e8ddc83c4 100644 --- a/dd-java-agent/instrumentation/build.gradle +++ b/dd-java-agent/instrumentation/build.gradle @@ -119,26 +119,15 @@ if (project.gradle.startParameter.taskNames.any { it.endsWith("generateMuzzleRep } tasks.named('shadowJar', ShadowJar) { - duplicatesStrategy = DuplicatesStrategy.FAIL - // Shadow 8.x silently wrote duplicate class (130+) entries into the instrumentation jar. - // Those duplicate entries did not survive as duplicates in the final agent jar, - // because `:dd-java-agent:expandAgentShadowJarInst` expands the instrumentation jar - // with a `Sync` task, and the specific `inst` expansion keeps the first duplicate path. - // (explicit `duplicatesStrategy = DuplicatesStrategy.EXCLUDE` on the `inst` prefix). + // Keep duplicate detection enabled on the aggregate instrumentation jar. // - // Shadow 9 refactored deeply it's "copy" pipeline via https://github.com/GradleUp/shadow/pull/1233 - // by `Project.zipTree` and can now honor `DuplicatesStrategy`, see in particular - // * https://github.com/GradleUp/shadow/issues/1223 - Duplicates from jars are not handled per duplicatesStrategy - // * https://github.com/GradleUp/shadow/issues/488 - DuplicatesStrategy.FAIL doesn't work with transform + // The previous source-set fixes and the servlet5 relocated-jar fix keep extra classes in + // producer jars without also publishing their raw output through `runtimeElements`. The old + // per-path `EXCLUDE` workaround is no longer needed. // - // Keeping `duplicatesStrategy = FAIL` only, Shadow 9 now fails while creating the instrumentation jar. - filesMatching([ - // jakarta-servlet-5.0 publishes the relocated advice jar as a runtime - // file dependency while also folding those classes into its own jar. - 'datadog/trace/instrumentation/servlet5/**/*.class', - ]) { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - } + // Shadow 9 honors `DuplicatesStrategy.FAIL` for these inputs. If a producer publishes the same + // classes from both its jar and a runtime file or directory, this task should fail again. + duplicatesStrategy = DuplicatesStrategy.FAIL dependencies { // the tracer is now in a separate shadow jar exclude(project(":dd-trace-core")) diff --git a/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle b/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle index 07a424a3113..cd4b16e8a02 100644 --- a/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle +++ b/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle @@ -51,7 +51,21 @@ tasks.register('relocatedJavaxJar', ShadowJar) { } dependencies { - implementation files(relocatedJavaxJar.outputs.files) + // Wire the relocated javax->jakarta advice into this project without publishing + // the relocated jar as runtime output: + // + // - keep the relocated jar visible to main compilation through `compileOnly` + // - keep the relocated jar visible to tests through `testImplementation` + // - include the relocated classes into this project's jar (jar task config) + // + // NOTE: + // Do not add the relocated jar to `implementation`: `runtimeElements` publishes + // both the jar artifact and runtime dependencies derived from `implementation`. + // Publishing the relocated jar there would expose the same servlet5 advice classes + // twice to downstream projects, once from the module jar and once from the + // relocated jar. + compileOnly files(relocatedJavaxJar.outputs.files) + testImplementation files(relocatedJavaxJar.outputs.files) compileOnly group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '5.0.0' testImplementation group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '5.0.0' testImplementation group: 'jakarta.servlet.jsp', name: 'jakarta.servlet.jsp-api', version: '3.0.0' From adcf141c6287ccfe7b24390828fc10f23806d0e1 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 3 Jul 2026 15:06:55 +0200 Subject: [PATCH 4/5] chore(build): Use lazy API on jakarta-servlet-5.0 shadow jar task --- .../servlet/jakarta-servlet-5.0/build.gradle | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle b/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle index cd4b16e8a02..7dc5f98d32c 100644 --- a/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle +++ b/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle @@ -30,7 +30,7 @@ configurations { javaxClassesToRelocate } -tasks.register('relocatedJavaxJar', ShadowJar) { +def relocatedJavaxJar = tasks.register('relocatedJavaxJar', ShadowJar) { relocate 'javax.servlet', 'jakarta.servlet' relocate 'datadog.trace.instrumentation.servlet3', 'datadog.trace.instrumentation.servlet5' relocate 'datadog.trace.instrumentation.servlet', 'datadog.trace.instrumentation.servlet5' @@ -49,6 +49,7 @@ tasks.register('relocatedJavaxJar', ShadowJar) { includeEmptyDirs = false } +def relocatedJavaxJarFile = relocatedJavaxJar.flatMap { it.archiveFile } dependencies { // Wire the relocated javax->jakarta advice into this project without publishing @@ -64,8 +65,8 @@ dependencies { // Publishing the relocated jar there would expose the same servlet5 advice classes // twice to downstream projects, once from the module jar and once from the // relocated jar. - compileOnly files(relocatedJavaxJar.outputs.files) - testImplementation files(relocatedJavaxJar.outputs.files) + compileOnly files(relocatedJavaxJarFile) + testImplementation files(relocatedJavaxJarFile) compileOnly group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '5.0.0' testImplementation group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '5.0.0' testImplementation group: 'jakarta.servlet.jsp', name: 'jakarta.servlet.jsp-api', version: '3.0.0' @@ -90,5 +91,5 @@ dependencies { } tasks.named("jar", Jar) { - from zipTree(relocatedJavaxJar.outputs.files.asPath) + from zipTree(relocatedJavaxJarFile) } From f873f22d1f9ad4821c905f2d07c3b512514d434f Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 3 Jul 2026 17:32:50 +0200 Subject: [PATCH 5/5] fix(build): Muzzle supports projects having jars outside runtimeClasspath Teach `MuzzleTask` to include an extra classpath (if provided). The goal is to allow it to see the relocated servlet5 jar. It is done by introducing `extraAgentClasspath` on the `MuzzelTask`. Another approach would have been to include the relcated jar on the runtimeClasspath: `sourceSets.main.runtimeClasspath += files(relocatedJavaxJarFile)` However this approach creates confusion ; anything that consume this configuration would see the relocated jar (e.g. during debug). --- .../datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt | 7 ++++++- .../servlet/jakarta-servlet-5.0/build.gradle | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt index 76917c6a6c7..b338e096456 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt @@ -7,6 +7,7 @@ import datadog.gradle.plugin.muzzle.MuzzleExtension import datadog.gradle.plugin.muzzle.allMainSourceSet import org.gradle.api.Project import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.FileCollection import org.gradle.api.file.RegularFileProperty import org.gradle.api.invocation.BuildInvocationDetails @@ -59,6 +60,10 @@ abstract class MuzzleTask @Inject constructor( @get:Classpath protected val agentClassPath = providers.provider { createAgentClassPath(project) } + @get:InputFiles + @get:Classpath + abstract val extraAgentClasspath: ConfigurableFileCollection + @get:InputFiles @get:Classpath protected val muzzleClassPath = providers.provider { createMuzzleClassPath(project, name) } @@ -119,7 +124,7 @@ abstract class MuzzleTask @Inject constructor( buildStartedTime.set(invocationDetails.buildStartedTime) bootstrapClassPath.setFrom(muzzleBootstrap) toolingClassPath.setFrom(muzzleTooling) - instrumentationClassPath.setFrom(agentClassPath.get()) + instrumentationClassPath.setFrom(agentClassPath.get(), extraAgentClasspath) testApplicationClassPath.setFrom(muzzleClassPath.get()) if (muzzleDirective != null) { assertPass.set(muzzleDirective.assertPass) diff --git a/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle b/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle index 7dc5f98d32c..c664a055f14 100644 --- a/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle +++ b/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/build.gradle @@ -1,4 +1,5 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import datadog.gradle.plugin.muzzle.tasks.MuzzleTask plugins { id 'java-test-fixtures' @@ -93,3 +94,10 @@ dependencies { tasks.named("jar", Jar) { from zipTree(relocatedJavaxJarFile) } + +// Make the relocated jar visible to muzzle through `extraAgentClasspath`. +// Muzzle uses the runtime classpath of a project, and this jar is produced +// outside of it. +tasks.withType(MuzzleTask).configureEach { + extraAgentClasspath.from(relocatedJavaxJarFile) +}