Gradle-Skripte sind vollwertige Programme, die komplexe Logik enthalten können. Fehler in Build-Skripten manifestieren sich oft subtil: Tasks produzieren falsche Outputs, Dependencies werden nicht korrekt aufgelöst, oder die Build-Performance degradiert ohne erkennbaren Grund. Effektives Debugging erfordert systematische Herangehensweise und Kenntnis der verfügbaren Werkzeuge.
Die grundlegendste Debugging-Technik ist gezieltes Logging. Gradle
bietet verschiedene Log-Level, die mit Command-Line-Parametern aktiviert
werden. Der Parameter --info zeigt zusätzliche
Informationen über Task-Ausführung und Dependency-Resolution. Mit
--debug wird umfangreiches Debugging-Output generiert, das
jeden Schritt der Build-Ausführung dokumentiert. Die Ausgabe ist
verbose, enthält aber wertvolle Details über Gradle-Interna.
Println-Debugging bleibt trotz seiner Einfachheit effektiv für Build-Skripte:
tasks.register("debugConfiguration") {
doFirst {
println("=== Configuration Debug ===")
println("Project: ${project.name}")
println("Version: ${project.version}")
println("BuildDir: ${layout.buildDirectory.get()}")
configurations.compileClasspath.get().resolvedConfiguration.resolvedArtifacts.forEach {
println("Dependency: ${it.id}")
}
}
}Diese Ausgaben erscheinen im Build-Output und helfen bei der
Verifikation von Konfigurationswerten. Die Platzierung in
doFirst oder doLast Blocks stellt sicher, dass
die Ausgabe während der Execution-Phase erfolgt, wenn alle Werte
aufgelöst sind.
Stack Traces bei Exceptions liefern wichtige Kontext-Information. Der
Parameter --stacktrace zeigt den vollständigen Stack Trace
bei Fehlern. Die Option --full-stacktrace inkludiert auch
Gradle-interne Frames, was bei der Diagnose von Plugin-Problemen hilft.
Diese Informationen sind essentiell für die Lokalisierung von Fehlern in
Custom Tasks oder Plugin-Code.
Die IDE-basierte Debugging ermöglicht Breakpoints, Variable-Inspection und Step-by-Step-Execution in Build-Skripten. Gradle startet dazu in einem Debug-Modus, der externe Debugger-Verbindungen akzeptiert. IntelliJ IDEA und Eclipse unterstützen Remote-Debugging von Gradle-Builds nativ.
Die Aktivierung erfolgt über JVM-Argumente:
export GRADLE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"
./gradlew buildDer suspend=y Parameter pausiert Gradle beim Start, bis
ein Debugger verbunden ist. Dies ermöglicht Debugging der frühen
Konfigurationsphase. Für Debugging der Execution-Phase kann
suspend=n verwendet werden.
Die IDE-Konfiguration für Remote-Debugging verbindet sich mit Port 5005:
// Breakpoint in Custom Task
abstract class DebugTask : DefaultTask() {
@TaskAction
fun execute() {
val config = project.configurations.getByName("implementation")
// Breakpoint hier - Inspection von config möglich
config.resolvedConfiguration.resolvedArtifacts.forEach { artifact ->
println("Processing: ${artifact.name}")
}
}
}Conditional Breakpoints reduzieren Noise bei häufig ausgeführtem Code. Ein Breakpoint, der nur für bestimmte Task-Namen triggert, fokussiert auf relevante Ausführungen. Die IDE-Watch-Expressions evaluieren Gradle-Properties und Provider-Werte während der Ausführung.
Build Scans sind Gradles umfassendstes Diagnose-Tool. Sie erfassen
detaillierte Informationen über jeden Build und präsentieren diese in
einer Web-Oberfläche. Die Aktivierung erfolgt mit dem
--scan Parameter, wobei die Daten an scans.gradle.com
übertragen werden. Für sensible Projekte bietet Gradle Enterprise
selbst-gehostete Build Scans.
Ein Build Scan dokumentiert die komplette Build-Ausführung: Task-Execution-Times, Dependency-Resolution, Configuration-Time, Test-Results und Console-Output. Die Timeline-Visualisierung zeigt parallele Task-Ausführung und identifiziert Bottlenecks. Die Dependency-Ansicht visualisiert den vollständigen Dependency-Graph mit Konflikt-Markierungen.
Custom Values erweitern Build Scans um projekt-spezifische Informationen:
buildScan {
value("Git Commit", executeGitCommand("rev-parse HEAD"))
value("CI Build", System.getenv("CI") ?: "false")
if (hasTask("test")) {
tag("tests")
}
buildFinished {
value("Build Duration", "${buildDuration}ms")
}
}Diese Werte erscheinen im Build Scan und ermöglichen Filterung und Analyse über mehrere Builds. Tags kategorisieren Builds für spätere Suche. Die Integration in CI/CD-Pipelines dokumentiert automatisch jeden Build mit reichhaltigen Kontext-Informationen.
Custom Tasks und Plugin-Logik erfordern automatisierte Tests für Zuverlässigkeit und Refactoring-Sicherheit. Gradle TestKit bietet eine Test-Infrastruktur für funktionale Tests von Build-Logik. Unit-Tests validieren Task-Behavior isoliert, während Integration-Tests komplette Build-Szenarien verifizieren.
Ein Unit-Test für einen Custom Task nutzt Gradles Project-Test-Fixtures:
class CustomTaskTest {
@Test
fun `task processes input correctly`() {
val project = ProjectBuilder.builder().build()
val task = project.tasks.create("testTask", ProcessingTask::class.java)
// Konfiguration
task.inputFile.set(project.layout.projectDirectory.file("test.txt"))
task.outputFile.set(project.layout.buildDirectory.file("output.txt"))
// Test-Datei erstellen
val testFile = project.file("test.txt")
testFile.writeText("test content")
// Ausführung
task.execute()
// Verifikation
val output = project.layout.buildDirectory.file("output.txt").get().asFile
assertEquals("PROCESSED: test content", output.readText())
}
}Der ProjectBuilder erstellt eine Test-Projekt-Instanz ohne vollständige Gradle-Infrastruktur. Dies ermöglicht schnelle, isolierte Tests von Task-Logik. Die Test-Umgebung unterstützt File-Operations, Configuration und Task-Creation, aber keine Cross-Project-Dependencies oder Plugin-Application.
Mock-Objects simulieren externe Dependencies:
class TaskWithExternalServiceTest {
@Test
fun `task handles service failure gracefully`() {
val project = ProjectBuilder.builder().build()
val task = project.tasks.create("apiTask", ApiTask::class.java)
val mockService = mock<ExternalService> {
on { fetchData() } doThrow RuntimeException("Service unavailable")
}
task.service = mockService
val result = assertDoesNotThrow {
task.executeWithFallback()
}
assertEquals("FALLBACK_DATA", result)
}
}TestKit ermöglicht End-to-End-Tests von Build-Skripten. Ein TestKit-Test erstellt temporäre Projekte, führt Gradle-Builds aus und verifiziert Ergebnisse. Diese Tests validieren Plugin-Behavior, Task-Integration und Build-Configuration in realistischen Szenarien.
Ein funktionaler Test mit TestKit:
class BuildLogicFunctionalTest {
@TempDir
lateinit var testProjectDir: File
private lateinit var buildFile: File
@BeforeEach
fun setup() {
buildFile = File(testProjectDir, "build.gradle.kts")
}
@Test
fun `custom task generates expected output`() {
buildFile.writeText("""
tasks.register("generateReport") {
val outputFile = layout.buildDirectory.file("report.txt")
outputs.file(outputFile)
doLast {
outputFile.get().asFile.writeText("Report Generated")
}
}
""".trimIndent())
val runner = GradleRunner.create()
.withProjectDir(testProjectDir)
.withArguments("generateReport", "--stacktrace")
.withPluginClasspath()
val result = runner.build()
assertTrue(result.output.contains("BUILD SUCCESSFUL"))
val reportFile = File(testProjectDir, "build/report.txt")
assertTrue(reportFile.exists())
assertEquals("Report Generated", reportFile.readText())
}
}TestKit-Tests können verschiedene Gradle-Versionen testen, was Kompatibilität sicherstellt:
@ParameterizedTest
@ValueSource(strings = ["7.0", "7.6", "8.0", "8.5"])
fun `plugin works with Gradle version`(gradleVersion: String) {
val runner = GradleRunner.create()
.withProjectDir(testProjectDir)
.withGradleVersion(gradleVersion)
.withArguments("build")
val result = runner.build()
assertEquals(TaskOutcome.SUCCESS, result.task(":build")?.outcome)
}Die Strukturierung von Build-Logik in testbare Einheiten verbessert Wartbarkeit. Komplexe Logik gehört in buildSrc oder separate Plugins, nicht in Build-Skripte. Funktionen extrahieren wiederverwendbare Patterns und ermöglichen isoliertes Testing:
// buildSrc/src/main/kotlin/BuildUtils.kt
object BuildUtils {
fun calculateVersion(baseVersion: String, isSnapshot: Boolean): String {
return if (isSnapshot) "$baseVersion-SNAPSHOT" else baseVersion
}
fun detectEnvironment(): String {
return when {
System.getenv("CI") != null -> "ci"
System.getProperty("idea.active") != null -> "ide"
else -> "local"
}
}
}Diese Utilities sind unit-testbar ohne Gradle-Kontext:
class BuildUtilsTest {
@Test
fun `version calculation respects snapshot flag`() {
assertEquals("1.0.0", BuildUtils.calculateVersion("1.0.0", false))
assertEquals("1.0.0-SNAPSHOT", BuildUtils.calculateVersion("1.0.0", true))
}
}Assertions in Build-Skripten validieren Vorbedingungen und Invarianten:
tasks.register("validateConfiguration") {
doFirst {
require(project.hasProperty("deployTarget")) {
"Property 'deployTarget' must be set for deployment"
}
val target = project.property("deployTarget").toString()
check(target in listOf("dev", "staging", "prod")) {
"Invalid deploy target: $target"
}
}
}Diese Assertions produzieren klare Fehlermeldungen bei Konfigurationsproblemen.
Continuous Testing von Build-Logik integriert Tests in die CI/CD-Pipeline. Ein dedizierter Job führt TestKit-Tests aus und verifiziert Build-Skript-Änderungen. Die Test-Coverage von Build-Logik wird gemessen und Mindest-Coverage enforced. Diese Praktiken behandeln Build-Code als First-Class-Code mit entsprechenden Qualitätsstandards.
mkdir gradle-debug-demo && cd gradle-debug-demo
gradle init --type basic --dsl kotlin --project-name debug-demo
mkdir -p src/main/java/demo
mkdir -p src/test/java/demoplugins {
java
application
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
repositories {
mavenCentral()
}
application {
mainClass.set("demo.Main")
}
// Dependencies absichtlich gemischt, um Debugging zu zeigen
dependencies {
implementation("com.google.guava:guava:32.1.3-jre")
implementation("org.apache.commons:commons-lang3:3.14.0")
testImplementation("junit:junit:4.13.2")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
// Beispiel-Task mit Debug-Ausgaben
tasks.register("debugConfiguration") {
doFirst {
println("=== DEBUG CONFIGURATION ===")
println("Project: ${project.name}")
println("Version: ${project.version}")
println("Java Toolchain: ${java.toolchain.languageVersion.get()}")
println("\n-- Dependencies (compileClasspath) --")
configurations.compileClasspath.get().resolvedConfiguration.resolvedArtifacts.forEach {
println(" ${it.id}")
}
}
}
// Task mit absichtlichem Fehler
tasks.register("failingTask") {
doLast {
println(">>> This will fail")
throw GradleException("Intentional error for debugging")
}
}
// Task für File-Operations mit Layout-API
abstract class GenerateFileTask : DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun generate() {
outputFile.get().asFile.writeText("Generated at ${System.currentTimeMillis()}")
println("File generated: ${outputFile.get().asFile}")
}
}
val genFile = tasks.register<GenerateFileTask>("generateFile") {
outputFile.set(layout.buildDirectory.file("demo/generated.txt"))
}
// Task, der das Ergebnis konsumiert
tasks.register("readGeneratedFile") {
val inputFile = genFile.flatMap { it.outputFile }
inputs.file(inputFile)
doLast {
println("Content of generated file: ${inputFile.get().asFile.readText()}")
}
}package demo;
import com.google.common.base.Joiner;
import org.apache.commons.lang3.StringUtils;
public class Main {
public static void main(String[] args) {
String joined = Joiner.on(", ").join("Gradle", "Debug", "Demo");
String capitalized = StringUtils.capitalize(joined);
System.out.println("Output: " + capitalized);
}
}package demo;
import org.junit.Test;
import static org.junit.Assert.*;
public class MainTest {
@Test
public void testString() {
String input = "gradle";
assertEquals("Gradle",
org.apache.commons.lang3.StringUtils.capitalize(input));
}
}./gradlew build --info
./gradlew build --debug
./gradlew build --warn./gradlew failingTask --stacktrace
./gradlew failingTask --full-stacktrace./gradlew dependencies
./gradlew dependencies --configuration compileClasspath
./gradlew dependencyInsight --dependency guava
./gradlew dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath./gradlew build --scan./gradlew build --profileErzeugt einen HTML-Report unter
build/reports/profile/.
export GRADLE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"
./gradlew buildDann mit der IDE über Port 5005 verbinden.
Mit diesem Projekt kannst du alle Debugging-Strategien aus dem Kapitel reproduzieren: