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/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 8077740656f..a3e8ddc83c4 100644 --- a/dd-java-agent/instrumentation/build.gradle +++ b/dd-java-agent/instrumentation/build.gradle @@ -119,31 +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([ - // 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', - // 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', - ]) { - 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..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' @@ -30,7 +31,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,9 +50,24 @@ tasks.register('relocatedJavaxJar', ShadowJar) { includeEmptyDirs = false } +def relocatedJavaxJarFile = relocatedJavaxJar.flatMap { it.archiveFile } 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(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' @@ -76,5 +92,12 @@ dependencies { } tasks.named("jar", Jar) { - from zipTree(relocatedJavaxJar.outputs.files.asPath) + 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) } 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) {