Skip to Content
Course content

120: The Gradle Build Lifecycle

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember the first time I tried to automate a version-stamping process in Gradle. I wanted to calculate a build timestamp and inject it into a properties file, so I just put a println and some logic right at the top of my build.gradle file to make sure it was working. I figured, "I'm running the jar task, so this code will run right before the JAR is built."

The "Why is this printing again?" moment

I wrote something simple. I didn't even create a custom task yet; I just put the logic in the open:


println "DEBUG: Calculating build version timestamp..."
version = "1.0-" + new Date().format("yyyyMMdd")

tasks.register("hello") {
    doLast {
        println "Hello from the task!"
    }
}

I ran ./gradlew hello. I expected to see the timestamp debug message, then the "Hello from the task!" message. That's what happened. Great. But then I ran ./gradlew tasks just to see my available tasks. To my surprise, I saw this in the console:


DEBUG: Calculating build version timestamp...

Tasks:
...

Wait. I didn't run the hello task. I didn't run a build. I just asked Gradle for a list of tasks, and it still executed my "Calculating build version" code. I thought I was losing my mind. I tried ./gradlew clean. Again: DEBUG: Calculating build version timestamp... appeared.

Wait, everything is a configuration?

This is the "aha!" moment where I realized I was fundamentally misunderstanding how Gradle works. I was treating the build.gradle file like a script that executes linearly from top to bottom only when a task is called. It's not. Gradle has a lifecycle, and my print statement was living in the Configuration Phase.

Gradle doesn't just jump to the task you requested. It has to build a "Project Object Model" first. It reads every single build.gradle file in the project to figure out what tasks exist, what their dependencies are, and how they are configured. Anything written outside of a task's action block (like doLast or doFirst) is executed during this phase.

If you put a heavy database call or a complex file-system scan at the top level of your build script, your build will feel sluggish every single time you run any command—even a simple --version check—because you're forcing Gradle to do that work during configuration.

Moving logic into the Execution Phase

I realized that calculating the version timestamp was something that should only happen if I'm actually building the project, not every time I check the task list. So, I moved the logic into a task action.


tasks.register("stampVersion") {
    doLast {
        println "DEBUG: Calculating build version timestamp..."
        // Logic to write to a file would go here
    }
}

tasks.named("jar") {
    dependsOn("stampVersion")
}

Now, when I run ./gradlew tasks, the debug message is gone. Silence. But when I run ./gradlew jar, I see:


:stampVersion
DEBUG: Calculating build version timestamp...
:jar

By wrapping the code in doLast, I moved it into the Execution Phase. This is the stage where Gradle actually runs the tasks that were identified as necessary based on the dependency graph it built during the Configuration phase.

The hidden first step: Initialization

Now, you might wonder, "How does Gradle even know which build.gradle files to look at?" That's the Initialization Phase. This happens before configuration. Gradle looks for a settings.gradle (or settings.gradle.kts) file.

If you have a multi-project build, this is where you define include 'app', 'library', 'core'. Gradle evaluates the settings file to determine which projects are part of the build. If you put a println in settings.gradle, you'll notice it prints even before the configuration prints. It's the very first domino to fall.

So, the mental model is: Initialization (What projects are we building?) → Configuration (What tasks exist and how are they linked?) → Execution (Run the specific tasks requested).




📋 Practical Task

Exercise: Fixing the "Heavy Configuration" Leak

You have been handed a legacy build script where a previous developer accidentally put a simulated "heavy" operation in the configuration phase, causing the build to lag every time any command is run. Your goal is to move this operation into the execution phase so it only runs when the buildArchive task is called.

Current build.gradle:


// Simulated heavy operation (Configuration Phase)
println "Starting heavy resource validation... (this is slowing down the build!)"
Thread.sleep(2000) 
println "Resource validation complete."

tasks.register("buildArchive") {
    doLast {
        println "Archiving project files..."
    }
}

Requirements:

  • Modify the script so that the "heavy resource validation" logic (including the Thread.sleep) only executes when ./gradlew buildArchive is run.
  • Ensure that running ./gradlew tasks no longer triggers the 2-second delay or the validation print statements.
  • The validation must happen before the archiving logic inside the buildArchive task executes.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.