diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleAction.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleAction.kt index 90c6fd2e274..d9d1793ca55 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleAction.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleAction.kt @@ -1,5 +1,6 @@ package datadog.gradle.plugin.muzzle +import org.gradle.api.GradleException import org.gradle.api.file.FileCollection import org.gradle.workers.WorkAction import java.lang.reflect.Method @@ -42,6 +43,12 @@ abstract class MuzzleAction : WorkAction { Boolean::class.java, String::class.java ) + try { assertionMethod.invoke(null, instCL, testCL, assertPass, muzzleDirective) + parameters.resultFile.get().asFile.writeText("PASSING") + } catch (e: Exception) { + parameters.resultFile.get().asFile.writeText(e.stackTraceToString()) + throw GradleException("Muzzle validation failed", e) + } } } diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirective.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirective.kt index f0948bc4457..d82e706e13d 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirective.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleDirective.kt @@ -1,11 +1,12 @@ package datadog.gradle.plugin.muzzle import org.eclipse.aether.repository.RemoteRepository +import java.io.Serializable /** * A pass or fail directive for a single dependency. */ -open class MuzzleDirective { +open class MuzzleDirective : Serializable { /** * Name is optional and is used to further define the scope of a directive. The motivation for this is that this * plugin creates a config for each of the dependencies under test with name '...---'. @@ -20,7 +21,7 @@ open class MuzzleDirective { var versions: String? = null var skipVersions: MutableSet = HashSet() var additionalDependencies: MutableList = ArrayList() - internal var additionalRepositories: MutableList = ArrayList() + internal var additionalRepositories: MutableList> = ArrayList() internal var excludedDependencies: MutableList = ArrayList() var assertPass: Boolean = false var assertInverse: Boolean = false @@ -51,7 +52,7 @@ open class MuzzleDirective { * @param type the type of repository, defaults to "default" */ fun extraRepository(id: String, url: String, type: String = "default") { - additionalRepositories.add(RemoteRepository.Builder(id, type, url).build()) + additionalRepositories.add(Triple(id, type, url)) } /** @@ -69,13 +70,15 @@ open class MuzzleDirective { * @param defaults the default repositories * @return a list of the default repositories followed by any additional repositories */ - fun getRepositories(defaults: List): List { + internal fun getRepositories(defaults: List): List { return if (additionalRepositories.isEmpty()) { defaults } else { ArrayList(defaults.size + additionalRepositories.size).apply { addAll(defaults) - addAll(additionalRepositories) + addAll(additionalRepositories.map { (id, type, url) -> + RemoteRepository.Builder(id, type, url).build() + }) } } } diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleExtension.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleExtension.kt index 7c51e1bc144..2bb88e8d69d 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleExtension.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleExtension.kt @@ -1,20 +1,20 @@ package datadog.gradle.plugin.muzzle -import org.eclipse.aether.repository.RemoteRepository import org.gradle.api.Action import org.gradle.api.model.ObjectFactory +import org.gradle.kotlin.dsl.newInstance import javax.inject.Inject import java.util.Locale /** * Muzzle extension containing all pass and fail directives. */ -abstract class MuzzleExtension @Inject constructor(protected val objectFactory: ObjectFactory) { +abstract class MuzzleExtension @Inject constructor(private val objectFactory: ObjectFactory) { val directives: MutableList = ArrayList() - private val additionalRepositories: MutableList = ArrayList() + private val additionalRepositories: MutableList> = ArrayList() fun pass(action: Action) { - val pass = objectFactory.newInstance(MuzzleDirective::class.java) + val pass = objectFactory.newInstance() action.execute(pass) postConstruct(pass) pass.assertPass = true @@ -22,7 +22,7 @@ abstract class MuzzleExtension @Inject constructor(protected val objectFactory: } fun fail(action: Action) { - val fail = objectFactory.newInstance(MuzzleDirective::class.java) + val fail = objectFactory.newInstance() action.execute(fail) postConstruct(fail) fail.assertPass = false @@ -39,7 +39,7 @@ abstract class MuzzleExtension @Inject constructor(protected val objectFactory: */ @JvmOverloads fun extraRepository(id: String, url: String, type: String = "default") { - additionalRepositories.add(RemoteRepository.Builder(id, type, url).build()) + additionalRepositories.add(Triple(id, type, url)) } private fun postConstruct(directive: MuzzleDirective) { 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 1e4961a01dd..b791f1f9a45 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt @@ -3,16 +3,17 @@ package datadog.gradle.plugin.muzzle import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.inverseOf import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.muzzleDirectiveToArtifacts import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.resolveVersionRange -import datadog.gradle.plugin.muzzle.MuzzleReportUtils.dumpVersionRanges -import datadog.gradle.plugin.muzzle.MuzzleReportUtils.mergeReports +import datadog.gradle.plugin.muzzle.tasks.MuzzleEndTask +import datadog.gradle.plugin.muzzle.tasks.MuzzleGenerateReportTask +import datadog.gradle.plugin.muzzle.tasks.MuzzleMergeReportsTask +import datadog.gradle.plugin.muzzle.tasks.MuzzleGetReferencesTask +import datadog.gradle.plugin.muzzle.tasks.MuzzleTask import org.eclipse.aether.artifact.Artifact 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.plugins.JavaBasePlugin -import org.gradle.api.plugins.JavaPlugin import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.exclude @@ -79,38 +80,27 @@ class MuzzlePlugin : Plugin { } val muzzleTask = project.tasks.register("muzzle") { - description = "Run instrumentation muzzle on compile time dependencies" - doLast { - if (!project.extensions.getByType().directives.any { it.assertPass }) { - project.logger.info("No muzzle pass directives configured. Asserting pass against instrumentation compile-time dependencies") - assertMuzzle(muzzleBootstrap, muzzleTooling, project) - } - } + this.muzzleBootstrap.set(muzzleBootstrap) + this.muzzleTooling.set(muzzleTooling) dependsOn(compileMuzzle) } - project.tasks.register("printReferences") { - description = "Print references created by instrumentation muzzle" - doLast { - printMuzzle(project) - } + project.tasks.register("printReferences") { dependsOn(compileMuzzle) + }.also { + val printReferencesTask = project.tasks.register("actuallyPrintReferences") { + doLast { + println(it.get().outputFile.get().asFile.readText()) + } + } + it.configure { finalizedBy(printReferencesTask) } } - project.tasks.register("generateMuzzleReport") { - description = "Print instrumentation version report" - doLast { - dumpVersionRanges(project) - } + project.tasks.register("generateMuzzleReport") { dependsOn(compileMuzzle) } - project.tasks.register("mergeMuzzleReports") { - description = "Merge generated version reports in one unique csv" - doLast { - mergeReports(project) - } - } + project.tasks.register("mergeMuzzleReports") val hasRelevantTask = project.gradle.startParameter.taskNames.any { taskName -> // removing leading ':' if present @@ -133,7 +123,7 @@ class MuzzlePlugin : Plugin { var runAfter: TaskProvider = muzzleTask project.extensions.getByType().directives.forEach { directive -> - project.logger.debug("configuring $directive") + project.logger.debug("configuring {}", directive) if (directive.isCoreJdk) { runAfter = addMuzzleTask(directive, null, project, runAfter, muzzleBootstrap, muzzleTooling) @@ -157,11 +147,8 @@ class MuzzlePlugin : Plugin { project.logger.info("configured $directive") } - val timingTask = project.tasks.register("muzzle-end") { - doLast { - val endTime = System.currentTimeMillis() - MuzzleReportUtils.generateResultsXML(project, endTime - startTime) - } + val timingTask = project.tasks.register("muzzle-end") { + startTimeMs.set(startTime) } // last muzzle task to run runAfter.configure { @@ -252,9 +239,9 @@ class MuzzlePlugin : Plugin { } val muzzleTask = instrumentationProject.tasks.register(muzzleTaskName) { - doLast { - assertMuzzle(muzzleBootstrap, muzzleTooling, instrumentationProject, muzzleDirective) - } + this.muzzleDirective.set(muzzleDirective) + this.muzzleBootstrap.set(muzzleBootstrap) + this.muzzleTooling.set(muzzleTooling) } runAfterTask.configure { diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtils.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtils.kt index 3e9cb0b3ab6..80051244788 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtils.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginUtils.kt @@ -5,8 +5,13 @@ import org.gradle.api.file.FileCollection import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME import org.gradle.api.tasks.SourceSetContainer +import org.gradle.build.event.BuildEventsListenerRegistry import org.gradle.kotlin.dsl.findByType import org.gradle.kotlin.dsl.getByType +import org.gradle.tooling.events.FinishEvent +import org.gradle.tooling.events.OperationCompletionListener +import org.gradle.tooling.events.task.TaskFinishEvent +import org.gradle.tooling.events.task.TaskSuccessResult internal val Project.mainSourceSet: SourceSet get() = extensions.findByType() @@ -17,3 +22,6 @@ internal val Project.allMainSourceSet: List get() = extensions.findByType() ?.filter { it.name.startsWith(MAIN_SOURCE_SET_NAME) } .orEmpty() + +internal val Project.pathSlug: String + get() = path.removePrefix(":").replace(':', '_') diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleReportUtils.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleReportUtils.kt deleted file mode 100644 index 1808a50b552..00000000000 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleReportUtils.kt +++ /dev/null @@ -1,132 +0,0 @@ -package datadog.gradle.plugin.muzzle - -import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.highest -import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.lowest -import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.resolveInstrumentationAndJarVersions -import org.eclipse.aether.RepositorySystem -import org.eclipse.aether.RepositorySystemSession -import org.eclipse.aether.util.version.GenericVersionScheme -import org.gradle.api.Project -import org.gradle.kotlin.dsl.getByType -import java.net.URLClassLoader -import java.util.SortedMap -import java.util.TreeMap -import java.util.function.BiFunction - -internal object MuzzleReportUtils { - private const val MUZZLE_DEPS_RESULTS = "muzzle-deps-results" - private const val MUZZLE_TEST_RESULTS = "muzzle-test-results" - - fun dumpVersionRanges(project: Project) { - val system: RepositorySystem = MuzzleMavenRepoUtils.newRepositorySystem() - val session: RepositorySystemSession = MuzzleMavenRepoUtils.newRepositorySystemSession(system) - val versions = TreeMap() - - project.extensions.getByType().directives - .filter { !it.isCoreJdk && !it.skipFromReport } - .forEach { directive -> - val range = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, session) - val cp = project.files(project.mainSourceSet.runtimeClasspath).map { it.toURI().toURL() }.toTypedArray() - val cl = URLClassLoader(cp, null) - val partials = resolveInstrumentationAndJarVersions(directive, cl, range.lowestVersion, range.highestVersion) - - partials.forEach { (key, value) -> - versions.merge(key, value, BiFunction { x, y -> - TestedArtifact( - x.instrumentation, x.group, x.module, - lowest(x.lowVersion, y.lowVersion), - highest(x.highVersion, y.highVersion) - ) - }) - } - } - dumpVersionsToCsv(project, versions) - } - - private fun dumpVersionsToCsv(project: Project, versions: SortedMap) { - val filename = project.path.replaceFirst("^:".toRegex(), "").replace(":", "_") - - val verrsionsFile = project.rootProject.layout.buildDirectory.file("$MUZZLE_DEPS_RESULTS/$filename.csv") - with(project.file(verrsionsFile)) { - parentFile.mkdirs() - writeText("instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion\n") - versions.values.forEach { - appendText( - listOf( - it.instrumentation, - it.group, - it.module, - it.lowVersion.toString(), - it.highVersion.toString() - ).joinToString(",") + "\n" - ) - } - project.logger.info("Wrote muzzle versions report to\n $this") - } - } - - /** - * Merges all muzzle report CSVs in the build directory into a single map and writes the merged results to a CSV. - */ - fun mergeReports(project: Project) { - val versionReports = project.fileTree(project.rootProject.layout.buildDirectory.dir(MUZZLE_DEPS_RESULTS)) { - include("*.csv") - } - - val map = TreeMap() - val versionScheme = GenericVersionScheme() - - versionReports.forEach { - project.logger.info("Processing muzzle report: $it") - it.useLines { lines -> - lines.forEachIndexed { idx, line -> - if (idx == 0) return@forEachIndexed // skip header - val split = line.split(",") - val parsed = TestedArtifact( - split[0], - split[1], - split[2], - versionScheme.parseVersion(split[3]), - versionScheme.parseVersion(split[4]) - ) - map.merge(parsed.key(), parsed) { x, y -> - TestedArtifact( - x.instrumentation, - x.group, - x.module, - MuzzleMavenRepoUtils.lowest(x.lowVersion, y.lowVersion), - MuzzleMavenRepoUtils.highest(x.highVersion, y.highVersion) - ) - } - } - } - } - - dumpVersionsToCsv(project, map) - } - - /** - * Generates a JUnit-style XML report for muzzle results. - */ - fun generateResultsXML(project: Project, millis: Long) { - val seconds = millis.toDouble() / 1000.0 - val name = "${project.path}:muzzle" - val dirname = name.replaceFirst("^:".toRegex(), "").replace(":", "_") - - val dir = project.rootProject.layout.buildDirectory.dir("$MUZZLE_TEST_RESULTS/$dirname/results.xml") - - with(project.file(dir)) { - parentFile.mkdirs() - writeText( - """ - - - - - - """.trimIndent() - ) - project.logger.info("Wrote muzzle results report to\n $this") - } - } -} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleTask.kt deleted file mode 100644 index 0a2fb159134..00000000000 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleTask.kt +++ /dev/null @@ -1,102 +0,0 @@ -package datadog.gradle.plugin.muzzle - -import org.gradle.api.DefaultTask -import org.gradle.api.NamedDomainObjectProvider -import org.gradle.api.Project -import org.gradle.api.artifacts.Configuration -import org.gradle.api.file.FileCollection -import org.gradle.api.invocation.BuildInvocationDetails -import org.gradle.api.tasks.SourceSet -import org.gradle.api.tasks.SourceSetContainer -import org.gradle.jvm.toolchain.JavaLanguageVersion -import org.gradle.jvm.toolchain.JavaToolchainService -import org.gradle.kotlin.dsl.getByType -import org.gradle.workers.WorkerExecutor -import java.lang.reflect.Method -import java.net.URLClassLoader -import javax.inject.Inject - -abstract class MuzzleTask : DefaultTask() { - init { - group = "Muzzle" - } - - @get:Inject - abstract val javaToolchainService: JavaToolchainService - - @get:Inject - abstract val invocationDetails: BuildInvocationDetails - - @get:Inject - abstract val workerExecutor: WorkerExecutor - - fun assertMuzzle( - muzzleBootstrap: NamedDomainObjectProvider, - muzzleTooling: NamedDomainObjectProvider, - instrumentationProject: Project, - muzzleDirective: MuzzleDirective? = null - ) { - val workQueue = if (muzzleDirective?.javaVersion != null) { - val javaLauncher = javaToolchainService.launcherFor { - languageVersion.set(JavaLanguageVersion.of(muzzleDirective.javaVersion!!)) - }.get() - workerExecutor.processIsolation { - forkOptions { - executable(javaLauncher.executablePath) - } - } - } else { - workerExecutor.noIsolation() - } - workQueue.submit(MuzzleAction::class.java) { - buildStartedTime.set(invocationDetails.buildStartedTime) - bootstrapClassPath.setFrom(muzzleBootstrap.get()) - toolingClassPath.setFrom(muzzleTooling.get()) - instrumentationClassPath.setFrom(createAgentClassPath(instrumentationProject)) - testApplicationClassPath.setFrom(createMuzzleClassPath(instrumentationProject, name)) - if (muzzleDirective != null) { - assertPass.set(muzzleDirective.assertPass) - this.muzzleDirective.set(muzzleDirective.name ?: muzzleDirective.module) - } else { - assertPass.set(true) - } - } - } - - fun printMuzzle(instrumentationProject: Project) { - val cp = instrumentationProject.mainSourceSet.runtimeClasspath - val cl = URLClassLoader(cp.map { it.toURI().toURL() }.toTypedArray(), null) - val printMethod: Method = cl.loadClass("datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin") - .getMethod( - "printMuzzleReferences", - ClassLoader::class.java - ) - printMethod.invoke(null, cl) - } - - private fun createAgentClassPath(project: Project): FileCollection { - project.logger.info("Creating agent classpath for $project") - val cp = project.files() - cp.from(project.allMainSourceSet.map { it.runtimeClasspath }) - - if (project.logger.isInfoEnabled) { - cp.forEach { project.logger.info("-- $it") } - } - return cp - } - - private fun createMuzzleClassPath(project: Project, muzzleTaskName: String): FileCollection { - project.logger.info("Creating muzzle classpath for $muzzleTaskName") - val cp = project.files() - val config = if (muzzleTaskName == "muzzle") { - project.configurations.named("compileClasspath").get() - } else { - project.configurations.named(muzzleTaskName).get() - } - cp.from(config) - if (project.logger.isInfoEnabled) { - cp.forEach { project.logger.info("-- $it") } - } - return cp - } -} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleWorkParameters.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleWorkParameters.kt index d9ae2e0bf68..5af13d9a10d 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleWorkParameters.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleWorkParameters.kt @@ -1,6 +1,7 @@ package datadog.gradle.plugin.muzzle import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.workers.WorkParameters @@ -12,5 +13,6 @@ interface MuzzleWorkParameters : WorkParameters { val testApplicationClassPath: ConfigurableFileCollection val assertPass: Property val muzzleDirective: Property + val resultFile: RegularFileProperty } diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/AbstractMuzzleReportTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/AbstractMuzzleReportTask.kt new file mode 100644 index 00000000000..c697e1c083d --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/AbstractMuzzleReportTask.kt @@ -0,0 +1,40 @@ +package datadog.gradle.plugin.muzzle.tasks + +import datadog.gradle.plugin.muzzle.TestedArtifact +import datadog.gradle.plugin.muzzle.pathSlug +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.OutputFile +import java.util.SortedMap + +abstract class AbstractMuzzleReportTask : AbstractMuzzleTask() { + @get:OutputFile + val versionsFile: Provider = project.rootProject + .layout + .buildDirectory + .file("$MUZZLE_DEPS_RESULTS/${project.pathSlug}.csv") + + internal fun dumpVersionsToCsv(versions: SortedMap) { + with(project.file(versionsFile)) { + parentFile.mkdirs() + writeText("instrumentation,jarGroupId,jarArtifactId,lowestVersion,highestVersion\n") + versions.values.forEach { + appendText( + listOf( + it.instrumentation, + it.group, + it.module, + it.lowVersion.toString(), + it.highVersion.toString() + ).joinToString(",") + "\n" + ) + } + project.logger.info("Wrote muzzle versions report to\n $this") + } + } + + companion object { + internal const val MUZZLE_DEPS_RESULTS = "muzzle-deps-results" + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/AbstractMuzzleTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/AbstractMuzzleTask.kt new file mode 100644 index 00000000000..f2dab2de15b --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/AbstractMuzzleTask.kt @@ -0,0 +1,9 @@ +package datadog.gradle.plugin.muzzle.tasks + +import org.gradle.api.DefaultTask + +abstract class AbstractMuzzleTask : DefaultTask() { + init { + group = "Muzzle" + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleEndTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleEndTask.kt new file mode 100644 index 00000000000..daf0ce44b06 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleEndTask.kt @@ -0,0 +1,40 @@ +package datadog.gradle.plugin.muzzle.tasks + +import datadog.gradle.plugin.muzzle.pathSlug +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +abstract class MuzzleEndTask : AbstractMuzzleTask() { + @get:Input + abstract val startTimeMs: Property + + @get:OutputFile + val resultsFile = project.rootProject + .layout + .buildDirectory + .file("${MUZZLE_TEST_RESULTS}/${project.pathSlug}_muzzle/results.xml") + + @TaskAction + fun generatesResultFile() { + val endTimeMs = System.currentTimeMillis() + val seconds = (endTimeMs - startTimeMs.get()).toDouble() / 1000.0 + with(project.file(resultsFile)) { + parentFile.mkdirs() + writeText( + """ + + + + + """.trimIndent() + ) + project.logger.info("Wrote muzzle results report to\n $this") + } + } + + companion object { + private const val MUZZLE_TEST_RESULTS = "muzzle-test-results" + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleGenerateReportTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleGenerateReportTask.kt new file mode 100644 index 00000000000..dce408ef247 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleGenerateReportTask.kt @@ -0,0 +1,53 @@ +package datadog.gradle.plugin.muzzle.tasks + +import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils +import datadog.gradle.plugin.muzzle.TestedArtifact +import org.eclipse.aether.util.version.GenericVersionScheme +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.TaskAction +import java.util.TreeMap + +abstract class MuzzleGenerateReportTask : AbstractMuzzleReportTask() { + init { + description = "Print instrumentation version report" + } + + private val versionReports = project.fileTree(project.rootProject.layout.buildDirectory.dir(MUZZLE_DEPS_RESULTS)) { + include("*.csv") + } + + /** + * Merges all muzzle report CSVs in the build directory into a single map and writes the merged results to a CSV. + */ + @TaskAction + fun mergeReports() { + val map = TreeMap() + val versionScheme = GenericVersionScheme() + versionReports.forEach { + project.logger.info("Processing muzzle report: $it") + it.useLines { lines -> + lines.forEachIndexed { idx, line -> + if (idx == 0) return@forEachIndexed // skip header + val split = line.split(",") + val parsed = TestedArtifact( + split[0], + split[1], + split[2], + versionScheme.parseVersion(split[3]), + versionScheme.parseVersion(split[4]) + ) + map.merge(parsed.key(), parsed) { x, y -> + TestedArtifact( + x.instrumentation, + x.group, + x.module, + MuzzleMavenRepoUtils.lowest(x.lowVersion, y.lowVersion), + MuzzleMavenRepoUtils.highest(x.highVersion, y.highVersion) + ) + } + } + } + } + dumpVersionsToCsv(map) + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleGetReferencesTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleGetReferencesTask.kt new file mode 100644 index 00000000000..bc79797558e --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleGetReferencesTask.kt @@ -0,0 +1,60 @@ +package datadog.gradle.plugin.muzzle.tasks + +import datadog.gradle.plugin.muzzle.mainSourceSet +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.build.event.BuildEventsListenerRegistry +import org.gradle.tooling.events.FinishEvent +import org.gradle.tooling.events.OperationCompletionListener +import org.gradle.tooling.events.task.TaskFinishEvent +import org.gradle.tooling.events.task.TaskSuccessResult +import java.io.PrintWriter +import java.io.StringWriter +import java.lang.reflect.Method +import java.net.URLClassLoader +import javax.inject.Inject + +@CacheableTask +abstract class MuzzleGetReferencesTask @Inject constructor( + providers: ProviderFactory, + objects: ObjectFactory, +) : AbstractMuzzleTask() { + + @get:Inject + abstract val buildEventsListenerRegistry: BuildEventsListenerRegistry + + init { + description = "Print references created by instrumentation muzzle" + outputs.upToDateWhen { true } + } + + @get:InputFiles + @get:Classpath + val classpath = providers.provider { project.mainSourceSet.runtimeClasspath } + + // This output is only used to make the task cacheable, this is not exposed + @get:OutputFile + val outputFile = objects.fileProperty().convention( + project.layout.buildDirectory.file("reports/references.txt") + ) + + @TaskAction + fun printMuzzle() { + val cl = URLClassLoader(classpath.get().map { it.toURI().toURL() }.toTypedArray(), null) + val printMethod: Method = cl.loadClass("datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin") + .getMethod( + "printMuzzleReferences", + ClassLoader::class.java, + PrintWriter::class.java, + ) + val stringWriter = StringWriter() + printMethod.invoke(null, cl, PrintWriter(stringWriter)) + + outputFile.get().asFile.writeText(stringWriter.toString()) + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleMergeReportsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleMergeReportsTask.kt new file mode 100644 index 00000000000..d443caeae08 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleMergeReportsTask.kt @@ -0,0 +1,49 @@ +package datadog.gradle.plugin.muzzle.tasks + +import datadog.gradle.plugin.muzzle.MuzzleExtension +import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils +import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.highest +import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.lowest +import datadog.gradle.plugin.muzzle.MuzzleMavenRepoUtils.resolveInstrumentationAndJarVersions +import datadog.gradle.plugin.muzzle.TestedArtifact +import datadog.gradle.plugin.muzzle.mainSourceSet +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.RepositorySystemSession +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.getByType +import java.net.URL +import java.net.URLClassLoader +import java.util.TreeMap +import java.util.function.BiFunction + +abstract class MuzzleMergeReportsTask : AbstractMuzzleReportTask() { + init { + description = "Print instrumentation version report" + } + + @TaskAction + fun dumpVersionRanges() { + val system: RepositorySystem = MuzzleMavenRepoUtils.newRepositorySystem() + val session: RepositorySystemSession = MuzzleMavenRepoUtils.newRepositorySystemSession(system) + val versions = TreeMap() + project.extensions.getByType().directives + .filter { !it.isCoreJdk && !it.skipFromReport } + .forEach { directive -> + val range = MuzzleMavenRepoUtils.resolveVersionRange(directive, system, session) + val cp = project.files(project.mainSourceSet.runtimeClasspath).map { it.toURI().toURL() }.toTypedArray() + val cl = URLClassLoader(cp, null) + val partials = resolveInstrumentationAndJarVersions(directive, cl, range.lowestVersion, range.highestVersion) + + partials.forEach { (key, value) -> + versions.merge(key, value, BiFunction { x, y -> + TestedArtifact( + x.instrumentation, x.group, x.module, + lowest(x.lowVersion, y.lowVersion), + highest(x.highVersion, y.highVersion) + ) + }) + } + } + dumpVersionsToCsv(versions) + } +} 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 new file mode 100644 index 00000000000..4f6d7193f43 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/tasks/MuzzleTask.kt @@ -0,0 +1,145 @@ +package datadog.gradle.plugin.muzzle.tasks + +import datadog.gradle.plugin.muzzle.MuzzleAction +import datadog.gradle.plugin.muzzle.MuzzleDirective +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.FileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.invocation.BuildInvocationDetails +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.jvm.toolchain.JavaToolchainService +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.property +import org.gradle.workers.WorkerExecutor +import javax.inject.Inject + +@CacheableTask +abstract class MuzzleTask @Inject constructor( + objects: ObjectFactory, + providers: ProviderFactory, +) : AbstractMuzzleTask() { + override fun getDescription(): String { + return if (muzzleDirective.isPresent) { + "Run instrumentation muzzle on ${muzzleDirective.get().name} dependency" + } else { + "Run instrumentation muzzle on compile time dependencies" + } + } + + @get:Inject + abstract val javaToolchainService: JavaToolchainService + + @get:Inject + abstract val invocationDetails: BuildInvocationDetails + + @get:Inject + abstract val workerExecutor: WorkerExecutor + + @get:InputFiles + @get:Classpath + abstract val muzzleBootstrap: Property + + @get:InputFiles + @get:Classpath + abstract val muzzleTooling: Property + + @get:InputFiles + @get:Classpath + protected val agentClassPath = providers.provider { createAgentClassPath(project) } + + @get:InputFiles + @get:Classpath + protected val muzzleClassPath = providers.provider { createMuzzleClassPath(project, name) } + + @get:Input + @get:Optional + val muzzleDirective: Property = objects.property() + + // This output is only used to make the task cacheable, this is not exposed + @get:OutputFile + @get:Optional + protected val result: RegularFileProperty = objects.fileProperty().convention( + project.layout.buildDirectory.file("reports/${name}.txt") + ) + + @TaskAction + fun muzzle() { + when { + !project.extensions.getByType().directives.any { it.assertPass } -> { + project.logger.info("No muzzle pass directives configured. Asserting pass against instrumentation compile-time dependencies") + assertMuzzle() + } + muzzleDirective.isPresent -> { + assertMuzzle(muzzleDirective.get()) + } + } + } + + private fun assertMuzzle(muzzleDirective: MuzzleDirective? = null) { + val workQueue = if (muzzleDirective?.javaVersion != null) { + val javaLauncher = javaToolchainService.launcherFor { + languageVersion.set(JavaLanguageVersion.of(muzzleDirective.javaVersion!!)) + }.get() + workerExecutor.processIsolation { + forkOptions { + executable(javaLauncher.executablePath) + } + } + } else { + workerExecutor.noIsolation() + } + workQueue.submit(MuzzleAction::class.java) { + buildStartedTime.set(invocationDetails.buildStartedTime) + bootstrapClassPath.setFrom(muzzleBootstrap) + toolingClassPath.setFrom(muzzleTooling) + instrumentationClassPath.setFrom(agentClassPath.get()) + testApplicationClassPath.setFrom(muzzleClassPath.get()) + if (muzzleDirective != null) { + assertPass.set(muzzleDirective.assertPass) + this.muzzleDirective.set(muzzleDirective.name ?: muzzleDirective.module) + } else { + assertPass.set(true) + } + resultFile.set(result) + } + } + + private fun createAgentClassPath(project: Project): FileCollection { + project.logger.info("Creating agent classpath for $project") + val cp = project.files() + cp.from(project.allMainSourceSet.map { it.runtimeClasspath }) + + if (project.logger.isInfoEnabled) { + cp.forEach { project.logger.info("-- $it") } + } + return cp + } + + private fun createMuzzleClassPath(project: Project, muzzleTaskName: String): FileCollection { + project.logger.info("Creating muzzle classpath for $muzzleTaskName") + val cp = project.files() + val config = if (muzzleTaskName == "muzzle") { + project.configurations.named("compileClasspath").get() + } else { + project.configurations.named(muzzleTaskName).get() + } + cp.from(config) + if (project.logger.isInfoEnabled) { + cp.forEach { project.logger.info("-- $it") } + } + return cp + } +} diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java index 65e99aa8e49..2d32e652c52 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleVersionScanPlugin.java @@ -8,6 +8,7 @@ import datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers; import de.thetaphi.forbiddenapis.SuppressForbidden; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -142,13 +143,14 @@ private static Map createHelperMap(final InstrumenterModule modu } @SuppressForbidden - public static void printMuzzleReferences(final ClassLoader instrumentationLoader) { + public static void printMuzzleReferences( + final ClassLoader instrumentationLoader, final PrintWriter out) { for (InstrumenterModule module : ServiceLoader.load(InstrumenterModule.class, instrumentationLoader)) { final ReferenceMatcher muzzle = module.getInstrumentationMuzzle(); - System.out.println(module.getClass().getName()); + out.println(module.getClass().getName()); for (final Reference ref : muzzle.getReferences()) { - System.out.println(prettyPrint(" ", ref)); + out.println(prettyPrint(" ", ref)); } } }