Skip to content
Merged
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
27 changes: 27 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,33 @@ test_published_artifacts:
paths:
- ./check_reports

validate_build:
extends: .gradle_build
stage: tests
needs: [ build ]
variables:
CACHE_TYPE: "lib"
script:
# Preserve the `build` job artifacts before Gradle rebuilds and overwrites them under
# workspace/**/build/libs, so jardiff can compare the rebuilt jars against them.
- mkdir -p reference-artifacts
- cp workspace/dd-java-agent/build/libs/*.jar reference-artifacts/
- cp workspace/dd-trace-api/build/libs/*.jar reference-artifacts/
- cp workspace/dd-trace-ot/build/libs/*.jar reference-artifacts/
- ./gradlew --version
# This will run the shadowJar task and exercise the build cache, allowing to identify build-cache issues
- ./gradlew compareToReferenceJar -PjardiffReferenceDir="$CI_PROJECT_DIR/reference-artifacts" -PskipTests $GRADLE_ARGS
Comment thread
bric3 marked this conversation as resolved.
Comment thread
bric3 marked this conversation as resolved.
after_script:
- source .gitlab/gitlab-utils.sh
- gitlab_section_start "collect-reports" "Collecting reports"
- .gitlab/collect_reports.sh --destination ./check_reports
- gitlab_section_end "collect-reports"
artifacts:
when: always
paths:
- ./check_reports
- '.gradle/daemon/*/*.out.log'

.check_job:
extends: .gradle_build
needs: [ build ]
Expand Down
5 changes: 5 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ gradlePlugin {
id = "dd-trace-java.sca-enrichments"
implementationClass = "datadog.gradle.plugin.sca.ScaEnrichmentsPlugin"
}

create("jardiff-plugin") {
id = "dd-trace-java.jardiff"
implementationClass = "datadog.gradle.plugin.jardiff.JardiffPlugin"
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package datadog.gradle.plugin.jardiff

import java.io.File

/**
* Pure, Gradle-free helpers for driving the [jardiff](https://github.com/bric3/jardiff) CLI.
*
* Kept separate from [JardiffTask] so the argument construction and exit-code interpretation can
* be unit-tested without spinning up a Gradle build or resolving the jardiff jar.
*/
object JardiffComparison {
/** Outcome of a jardiff run, derived from its process exit value. */
enum class Outcome {
/** Exit 0 — the two jars are identical (for the selected include/exclude set). */
IDENTICAL,

/** Exit 1 — jardiff reported differences (behaves like `diff(1)` under `--exit-code`). */
DIFFERENT,

/** Any other exit value — jardiff itself failed (bad arguments, unreadable jar, ...). */
ERROR,
}

/**
* Builds the jardiff argument list comparing [reference] (left) against [candidate] (right).
*
* [mode] selects the output mode (e.g. `--stat` for a `git diff --stat`-like summary, `--status`,
* or blank for the default full diff). `--exit-code` is always added.
* The [JardiffTask] relies on the process exit value.
* [includes]/[excludes] are comma-joined glob patterns (empty means comparing every entry), and
* [additionalOptions] are passed through verbatim, right before the two jars.
*/
fun buildArguments(
reference: File,
candidate: File,
mode: String,
includes: List<String> = emptyList(),
excludes: List<String> = emptyList(),
additionalOptions: List<String> = emptyList(),
): List<String> = buildList {
if (mode.isNotBlank()) {
add(mode)
}
add("--exit-code")
add("--color=never")
if (includes.isNotEmpty()) {
add("--include=" + includes.joinToString(","))
}
if (excludes.isNotEmpty()) {
add("--exclude=" + excludes.joinToString(","))
}
addAll(additionalOptions)
// jardiff positional arguments: <left> <right>.
// The reference (the artifact validated by the build job) is the left/baseline side,
// the freshly built candidate is the right side.
add(reference.absolutePath)
add(candidate.absolutePath)
}

/** Maps a jardiff process exit value to an [Outcome]. */
fun outcomeOf(exitValue: Int): Outcome = when (exitValue) {
0 -> Outcome.IDENTICAL
1 -> Outcome.DIFFERENT
else -> Outcome.ERROR
}

/**
* Renders the equivalent `java -cp … <mainClass> <arguments>` shell command, so the comparison
* can be copy-pasted and reproduced outside Gradle. Tokens containing shell-significant
* characters (spaces, globs, commas, ...) are single-quoted.
*/
fun shellCommandLine(classpath: Iterable<File>, mainClass: String, arguments: List<String>): String {
val classpathValue = classpath.joinToString(File.pathSeparator) { it.absolutePath }
return (listOf("java", "-cp", classpathValue, mainClass) + arguments)
.joinToString(" ", transform = ::shellQuote)
}

private fun shellQuote(token: String): String =
if (token.isNotEmpty() && token.all { it.isLetterOrDigit() || it in "/._-=:" }) {
token
} else {
"'" + token.replace("'", "'\\''") + "'"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package datadog.gradle.plugin.jardiff

import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property

/** Configuration for the [JardiffPlugin]. */
interface JardiffExtension {
/**
* Maven coordinate of the jardiff CLI resolved to run the comparison.
* Defaults to [JardiffPlugin.DEFAULT_TOOL_COORDINATE].
*/
val toolCoordinate: Property<String>

/**
* Fully qualified main class of the jardiff CLI.
* Defaults to [JardiffPlugin.DEFAULT_MAIN_CLASS].
*/
val mainClass: Property<String>

/**
* jardiff output mode flag, e.g. `--stat` or `--status`; blank selects the default full diff.
* Defaults to [JardiffPlugin.DEFAULT_MODE].
*/
val mode: Property<String>

/**
* Extra jardiff options passed verbatim, right before the two jars (e.g. `--ignore-member-order`
* or `--class-text-producer=javap`). Empty by default.
*/
val additionalOptions: ListProperty<String>

/**
* Directory receiving jardiff reports. Defaults to `build/reports/jardiff` in the target project.
*/
val reportDir: DirectoryProperty

/**
* When true, tolerate mismatching candidate and reference jar hashes if jardiff reports no
* differences. Defaults to false.
*/
val ignoreHashCheck: Property<Boolean>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package datadog.gradle.plugin.jardiff

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.named
import org.gradle.kotlin.dsl.register

/**
* Registers the `compareToReferenceJar` task ([JardiffTask]) and wires its reusable inputs:
* - the jardiff CLI classpath (resolved from [JardiffExtension.toolCoordinate]),
* - the candidate archive — the module's main publishable jar (the shadow jar when the shadow
* plugin is applied, otherwise the plain jar),
* - the reference jar, resolved from the `-PjardiffReferenceDir` project property by matching the
* candidate's file name inside that directory.
*
* Apply it with `id("dd-trace-java.jardiff")`.
*/
class JardiffPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create<JardiffExtension>("jardiff")
extension.toolCoordinate.convention(DEFAULT_TOOL_COORDINATE)
extension.mainClass.convention(DEFAULT_MAIN_CLASS)
extension.mode.convention(DEFAULT_MODE)
extension.additionalOptions.convention(emptyList())
extension.reportDir.convention(project.layout.buildDirectory.dir("reports/jardiff"))
extension.ignoreHashCheck.convention(false)

// Use a detached configuration (created here, resolved only when the task runs)
//
// This keeps the jardiff artifact out of `lockAllConfigurations()` dependency locking.
// The dependency is added lazily, so overriding `jardiff.toolCoordinate` still takes effect.
// The `@jar` requests the artifact only, because jardiff ships a self-contained "fat" CLI jar
// under a non-default Gradle Module Metadata variant, the default resolution misses it.
// Appending `@jar` ignores metadata and fetches that jar.
val toolClasspath = project.configurations.detachedConfiguration().apply {
dependencies.addLater(
extension.toolCoordinate.map { coordinate ->
val artifactOnly = if ('@' in coordinate) coordinate else "$coordinate@jar"
project.dependencies.create(artifactOnly)
},
)
}

val projectDirectory = project.layout.projectDirectory
val referenceDirProperty =
project.providers.gradleProperty("jardiffReferenceDir").filter { it.isNotBlank() }

val compare = project.tasks.register<JardiffTask>(COMPARE_TASK_NAME) {
group = "verification"
description = "Compares the built jar against a reference jar (typically the CI `build` " +
"job artifact) using jardiff, failing if they differ. Set the reference with " +
"--reference-jar=<path> or -PjardiffReferenceDir=<dir>."
jardiffClasspath.convention(toolClasspath)
mainClass.convention(extension.mainClass)
mode.convention(extension.mode)
additionalOptions.convention(extension.additionalOptions)
reportDir.convention(extension.reportDir)
ignoreHashCheck.convention(extension.ignoreHashCheck)
// Ignore **/*.version by default, except under CI where the build and deploy
// jobs share the same commit.
ignoreVersionFiles.convention(
project.providers.environmentVariable("CI").map { false }.orElse(true),
)
referenceJar.convention(
// Use the same name as the candidate jar
referenceDirProperty.flatMap { dir ->
candidateJar.map { candidate -> projectDirectory.dir(dir).file(candidate.asFile.name) }
},
)
}

// candidateJar = the module's main publishable archive
project.pluginManager.withPlugin("java") {
compare.configure {
candidateJar.convention(
project.tasks.named<AbstractArchiveTask>("jar").flatMap { it.archiveFile },
)
}
}
project.pluginManager.withPlugin("com.gradleup.shadow") {
compare.configure {
candidateJar.set(
project.tasks.named<AbstractArchiveTask>("shadowJar").flatMap { it.archiveFile },
)
}
}
}

companion object {
const val DEFAULT_TOOL_COORDINATE = "io.github.bric3.jardiff:jardiff-cli:0.2.0"

const val DEFAULT_MAIN_CLASS = "io.github.bric3.jardiff.app.Main"

const val DEFAULT_MODE = "--stat"

const val COMPARE_TASK_NAME = "compareToReferenceJar"
}
}
Loading